开始之前
上篇文章已经学习了如何认证,本篇将深入学习如何授权,如果需要继续理解认证的,包括基础认证,JWT 认证,以及如何在 .Net 项目中使用认证的,都可以看 上篇文章。
更多学习内容,可以看我的 .NET 学习之路系列-认证与授权。
授权
授权通常是针对用户可执行的操作。在 .NET 的解决方案中,授权的使用是非常简单的。它通过 AuthorizeAttribute
和其各种参数来控制。
所有的授权都是在认证之后的,如果开启了授权,而没有提供认证方案,则会报错。
下面的代码,都默认已经配置好了 JWT 认证方案。
授权的使用
其最简单的形式就是将 [Authrize]
属性应用于控制器、操作或 Razor 页面。
[Authorize]
public class AccountController : Controller
{
public ActionResult Login()
{
}
public ActionResult Logout()
{
}
}
如果要对操作应用授权而非控制器,则置于操作本身即可:
public class AccountController : Controller
{
public ActionResult Login()
{
}
[Authorize]
public ActionResult Logout()
{
}
}
对于控制器需要权限,而里面某些应用不需要授权的情况,使用 [AllowAnonymous]
属性进行控制:
[Authorize]
public class AccountController : Controller
{
[AllowAnonymous]
public ActionResult Login()
{
}
public ActionResult Logout()
{
}
}
AllowAnonymous
属性会跳过所有授权语句,如果它与 Authorize
组合使用,则会忽略 Authorize
属性,并且该效果向下兼容。
基于角色授权
想要使用角色授权,首先要在令牌中添加角色:
具体代码可以看 示例代码
添加角色
var token = new JwtSecurityToken(
issuer: "jeremyjone@qq.com",
audience: "jeremyjone",
expires: DateTime.UtcNow.AddHours(1),
signingCredentials: new SigningCredentials(key, SecurityAlgorithms.HmacSha256),
claims: new Claim[]
{
// 角色需要在这里填写
new Claim(ClaimTypes.Role, "Admin"),
// 多个角色可以重复写,生成的 JWT 会是一个数组
new Claim(ClaimTypes.Role, "Super")
});
使用角色进行授权
在 [Authorize]
后面跟上角色参数,即可使用角色授权。
// 只有角色为 Admin 的用户可以使用
[Authorize(Roles = "Admin")]
public IEnumerable<WeatherForecast> Get()
{
}
如果想使用两个或以上的角色,则可以:
// 多个角色使用逗号(,)分开,它们是 “或” 的关系
[Authorize(Roles = "Admin,User")]
public IEnumerable<WeatherForecast> Get()
{
}
// 多个角色分开写是 “且” 的关系
[Authorize(Roles = "Admin")]
[Authorize(Roles = "Super")]
public IEnumerable<WeatherForecast> Get()
{
}
基于策略授权
具体代码可以看 示例代码
上面的角色过于简单,.NET 提供了更加强大的功能--基于策略。
简单来说,就是将上面的 Roles
改为 Policy
即可。
[Authorize(Policy = "AdminAndSuper")]
public IEnumerable<WeatherForecast> Get()
{
}
策略授权使用起来确实方便很多,但是其配置也更复杂。
配置角色策略
在 Startup.ConfigureServices 中添加授权服务:
services.AddAuthorization(options =>
{
// 配置角色策略
options.AddPolicy("AdminAndSuper", ploicy => policy.RequireRole("Admin").RequireRole("Super"));
options.AddPolicy("AdminOrSuper", ploicy => policy.RequireRole("Admin", "Super"));
});
关键通过 RequireRole
方法可以实现匹配角色,多参数为“或”的关系,链式为“且”的关系。
配置声明策略
声明策略通过匹配令牌中的声明(Claim)进行授权。其方式与角色策略的配置方式差不多:
services.AddAuthorization(options =>
{
// 创建声明的策略
options.AddPolicy("EmployeeOnly", policy => policy.RequireClaim("EmployeeNo"));
options.AddPolicy("Founders", policy =>
// 给声明添加指定允许值的列表
policy.RequireClaim("EmployeeNo", "1", "2", "3", "4", "5"));
});
配置自定义策略
上面的方式还是比较简单,有时候需要一些更为复杂的方案,比如是否存在某一个声明属性,或者进行一些更高级的判断等,这时候就需要自定义。
比如实现一个查询是否包含某一声明属性:
services.AddAuthorization(options =>
{
// 自定义策略
options.AddPolicy("HasBirthDay",
// 实现一个简易的自定义策略
policy => policy.RequireAssertion(context =>
context.User.HasClaim(c => c.Type == ClaimTypes.DateOfBirth)));
});
创建自定义策略类
这样还是不方便,则可以使用自定义策略类来处理:
services.AddAuthorization(options =>
{
// 继承自 IAuthorizationRequirement 接口的策略
options.AddPolicy("AtLeast18", policy =>
// 实现一个至少18岁的策略
policy.Requirements.Add(new MinimumAgeRequirement(18)));
});
创建一个 MinimumAgeRequirement
类:
/// <summary>
/// 最小年龄策略要求类,它需要实现 IAuthorizationRequirement 接口
/// </summary>
public class MinimumAgeRequirement : IAuthorizationRequirement
{
public MinimumAgeRequirement(int minAge)
{
MinAge = minAge;
}
public int MinAge { get; }
}
然后创建一个针对我们需要的策略(至少 18 岁)进行处理。创建一个 MinimumAgeHandle
类,它继承自 AuthorizationHandler<TRequirement>
抽象类,TRequirement
是一个泛型,这里应该使用 MinimumAgeRequirement
进行替换。
/// <summary>
/// 最小年龄的处理类,它需要实现 AuthorizationHandler<TRequirement> 抽象类
/// </summary>
public class MinimumAgeHandler : AuthorizationHandler<MinimumAgeRequirement>
{
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context,
MinimumAgeRequirement requirement)
{
if (!context.User.HasClaim(c => c.Type == ClaimTypes.DateOfBirth))
{
return Task.CompletedTask;
}
var dateOfBirth = context.User.FindFirstValue(ClaimTypes.DateOfBirth);
if (string.IsNullOrWhiteSpace(dateOfBirth)) return Task.CompletedTask;
var birth = Convert.ToDateTime(dateOfBirth);
var age = DateTime.Today.Year - birth.Year;
if (birth > DateTime.Today.AddYears(-age)) age--;
if (age >= requirement.MinAge)
// Succeed 方法是验证成功的必要语句
context.Succeed(requirement);
return Task.CompletedTask;
}
}
最后需要将操作类在服务中注入:
// 注册自定义策略的处理程序
services.AddSingleton<IAuthorizationHandler, MinimumAgeHandler>();
然后,我们可以将控制器中的策略改为 [Authorize(Policy = "AtLeast18")]
以查看效果。如果令牌中包含年龄,并且年龄大于 18 岁则会正常进入,反之不会。
在策略中使用参数
上面的例子中,年龄是写死的,这样对控制器应用非常不友好,如果要获取很多不同年龄段的权限,需要配置对应多项的策略,这样就很麻烦。好在 .NET 给我们提供了一个 Provider,让我们可以方便配置。
具体代码可以看 示例代码
基于上例,继续创建一个 MinimumAgeAuthorizeAttribute
类,它继承自 AuthorizeAttribute
:
/// <summary>
/// 通过将参数映射到一个字符串,用于检索相应的授权策略。
/// </summary>
internal class MinimumAgeAuthorizeAttribute : AuthorizeAttribute
{
private const string POLICY_PREFIX = "MinimumAge";
public MinimumAgeAuthorizeAttribute(int age) => Age = age;
public int Age
{
get => int.TryParse(Policy.Substring(POLICY_PREFIX.Length), out var age) ? age : default;
set => Policy = $"{POLICY_PREFIX}{value.ToString()}";
}
}
写完了它,在控制器就可以使用 [MinimumAgeAuthorize(18)]
属性了。
但是还没有对应的处理程序,它并不能真正运行起来。该属性仅仅是收集授权参数为一个特定字符串,我们需要通过程序将该字符串解析为一个年龄数字,然后判断数字是不是符合标准。
创建一个 MinimumAgePolicyProvider
类,它继承自 IAuthorizationPolicyProvider
:
/// <summary>
/// 提供最小年龄的授权服务,通过 IAuthorizationPolicyProvider 接口,实现 GetPolicyAsync 方法即可。
/// </summary>
internal class MinimumAgePolicyProvider: IAuthorizationPolicyProvider
{
private const string POLICY_PREFIX = "MinimumAge";
public DefaultAuthorizationPolicyProvider FallbackPolicyProvider { get; }
public MinimumAgePolicyProvider(IOptions<AuthorizationOptions> options)
{
// 提供其他授权策略方案,这里使用默认方案
FallbackPolicyProvider = new DefaultAuthorizationPolicyProvider(options);
}
/// <summary>
/// 获取策略并进行处理
/// </summary>
/// <param name="policyName"></param>
/// <returns></returns>
public Task<AuthorizationPolicy> GetPolicyAsync(string policyName)
{
// 获取策略字符串并解析出年龄
if (policyName.StartsWith(POLICY_PREFIX, StringComparison.OrdinalIgnoreCase) &&
int.TryParse(policyName.Substring(POLICY_PREFIX.Length), out var age))
{
// 这里其实和 Startup.ConfigureService 中的配置差不多,都是添加一个 Requirement
var policy = new AuthorizationPolicyBuilder();
policy.AddRequirements(new MinimumAgeRequirement(age));
return Task.FromResult(policy.Build());
}
//return Task.FromResult<AuthorizationPolicy>(null);
// 当上述授权流程出现问题(例如获取不到年龄),则使用该备选方案
return FallbackPolicyProvider.GetPolicyAsync(policyName);
}
//public Task<AuthorizationPolicy> GetDefaultPolicyAsync()
//{
// throw new NotImplementedException();
//}
//public Task<AuthorizationPolicy> GetFallbackPolicyAsync()
//{
// throw new NotImplementedException();
//}
public Task<AuthorizationPolicy> GetDefaultPolicyAsync() => FallbackPolicyProvider.GetDefaultPolicyAsync();
public Task<AuthorizationPolicy> GetFallbackPolicyAsync() => FallbackPolicyProvider.GetFallbackPolicyAsync();
}
最后,将 Handler 和 Provider 都注册到服务中:
// 注入授权服务
services.AddSingleton<IAuthorizationHandler, MinimumAgeHandler>();
services.AddSingleton<IAuthorizationPolicyProvider, MinimumAgePolicyProvider>();
这样再启动服务器,就可以进行正常而灵活的授权了。
文章评论