ASP.NET Core 2.0 Web API 基于 JWT 的自定义策略授权详解
在现代 Web 应用开发中,前后端分离的架构已成为主流。ASP.NET Core 是构建高性能、跨平台 Web API 的绝佳框架。在 API 的安全领域,基于声明的授权(Claims-Based Authorization)因其灵活性和可扩展性而被广泛采用,而 JSON Web Tokens 则是实现这种授权模式的流行标准。
ASP.NET Core 内置了一个强大且基于策略的授权系统。虽然框架提供了一些简单的授权特性(如 [Authorize(Roles = "Admin")]),但对于复杂的业务场景,自定义授权策略才是最佳选择。本文将深入探讨如何在 ASP.NET Core 2.0 Web API 中,结合 JWT 认证,实现灵活且强大的自定义策略授权。
目录#
- 核心概念解析
- 项目搭建与基础配置
- 创建项目与引入依赖
- 配置 JWT 认证服务
- 实现自定义策略授权
- 在控制器中使用授权
- 最佳实践与常见问题
- JWT 安全最佳实践
- 策略设计建议
- 常见问题排查
- 总结
- 参考
核心概念解析#
JWT (JSON Web Token)#
JWT 是一种开放标准(RFC 7519),用于在各方之间安全地传输信息作为 JSON 对象。由于其具有数字签名(使用 HMAC 或 RSA),这些信息是可信的。一个 JWT 通常由三部分组成:Header.Payload.Signature。
- Payload (负载):包含我们关心的“声明”。声明是关于实体(通常是用户)和其他数据的语句。例如,用户的 ID、角色、部门等信息都可以作为声明存入 Payload。
- 特点:无状态、自包含、易于跨域共享。
认证 (Authentication) 与授权 (Authorization)#
- 认证:解决“你是谁?”的问题。是验证用户身份的过程(例如,通过用户名和密码登录)。在本文中,我们使用 JWT Bearer Token 来认证用户。
- 授权:解决“你能做什么?”的问题。是确认已认证用户是否有权限执行特定操作的过程。本文的核心就是通过自定义策略来实现复杂的授权逻辑。
策略 (Policy) 与要求 (Requirement)#
ASP.NET Core 的授权系统围绕“策略”构建。
- 策略:由一个或多个“要求”组成。可以将其视为一个权限单元或一个访问规则。例如,一个名为
"Over18Only"的策略可能包含一个"MinimumAgeRequirement"。 - 要求:策略的基本组成部分,代表授权必须满足的一个条件。处理程序负责评估这些要求。
- 处理程序:负责检查要求是否得到满足。一个要求可以有多个处理程序。
项目搭建与基础配置#
创建项目与引入依赖#
- 使用 Visual Studio 或 CLI 创建一个新的 ASP.NET Core Web API 项目。
- 通过 NuGet 安装必要的包:
Install-Package Microsoft.AspNetCore.Authentication.JwtBearer Install-Package System.IdentityModel.Tokens.Jwt
配置 JWT 认证服务#
在 Startup.cs 文件的 ConfigureServices 方法中,添加 JWT 认证服务。
public void ConfigureServices(IServiceCollection services)
{
// ... 其他服务配置,如 AddMvc() 等
// 1. 配置 JWT 设置,通常从 appsettings.json 中读取
var jwtSettings = Configuration.GetSection("JwtSettings");
var secretKey = Encoding.ASCII.GetBytes(jwtSettings.GetValue<string>("SecretKey"));
services.Configure<JwtSettings>(jwtSettings);
// 2. 添加认证服务
services.AddAuthentication(options =>
{
// 设置默认的认证方案为 JWT Bearer
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
options.RequireHttpsMetadata = false; // 生产环境应为 true
options.SaveToken = true;
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(secretKey), // 使用密钥验证签名
ValidateIssuer = true,
ValidIssuer = jwtSettings.GetValue<string>("Issuer"), // 验证签发者
ValidateAudience = true,
ValidAudience = jwtSettings.GetValue<string>("Audience"), // 验证接收者
ValidateLifetime = true, // 验证令牌有效期
ClockSkew = TimeSpan.Zero // 容忍的时间偏差(零表示不容忍)
};
});
// 授权配置将在下一步添加
services.AddAuthorization();
// ... 其他配置
}
// JWT 配置模型
public class JwtSettings
{
public string SecretKey { get; set; }
public string Issuer { get; set; }
public string Audience { get; set; }
public int ExpireMinutes { get; set; }
}在 appsettings.json 中添加配置:
{
"JwtSettings": {
"SecretKey": "ThisIsASuperLongSecretKeyForSigningTheJwtToken!",
"Issuer": "MyWebApi",
"Audience": "MyWebApiClients",
"ExpireMinutes": 60
}
}实现自定义策略授权#
场景定义:基于部门与职级的权限控制#
假设我们有一个员工管理系统 API。权限规则如下:
- 用户有一个
Department声明(如 "HR", "IT")和一个Rank声明(如 "Staff", "Manager")。 - 策略1:
"DepartmentPolicy":要求用户必须属于某个特定部门(例如,只能访问自己部门的资源)。 - 策略2:
"RankPolicy":要求用户的职级必须不低于某个级别(例如,只有经理及以上才能删除资源)。
步骤 1:定义自定义授权要求 (Requirement)#
要求是一个简单的类,它包含策略评估所需的数据。
using Microsoft.AspNetCore.Authorization;
// 部门要求:要求用户必须属于某个特定部门
public class DepartmentRequirement : IAuthorizationRequirement
{
public string RequiredDepartment { get; }
public DepartmentRequirement(string requiredDepartment)
{
RequiredDepartment = requiredDepartment;
}
}
// 职级要求:要求用户的职级必须达到某个级别
// 假设职级用数字表示,越大级别越高(例如:1-Staff, 2-Manager, 3-Director)
public class MinimumRankRequirement : IAuthorizationRequirement
{
public int MinimumRank { get; }
public MinimumRankRequirement(int minimumRank)
{
MinimumRank = minimumRank;
}
}步骤 2:实现授权处理程序 (Authorization Handler)#
处理程序包含授权逻辑,它检查用户的声明是否满足要求。
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
// 处理 DepartmentRequirement
public class DepartmentHandler : AuthorizationHandler<DepartmentRequirement>
{
protected override Task HandleRequirementAsync(
AuthorizationHandlerContext context,
DepartmentRequirement requirement)
{
// 1. 检查用户是否认证
if (!context.User.Identity.IsAuthenticated)
{
context.Fail();
return Task.CompletedTask;
}
// 2. 从用户的声明中查找 "Department" 声明
var departmentClaim = context.User.Claims
.FirstOrDefault(c => c.Type == "Department");
// 3. 进行逻辑判断
if (departmentClaim != null && departmentClaim.Value == requirement.RequiredDepartment)
{
// 声明匹配,授权成功
context.Succeed(requirement);
}
else
{
// 声明不匹配,授权失败
context.Fail();
}
return Task.CompletedTask;
}
}
// 处理 MinimumRankRequirement
public class MinimumRankHandler : AuthorizationHandler<MinimumRankRequirement>
{
protected override Task HandleRequirementAsync(
AuthorizationHandlerContext context,
MinimumRankRequirement requirement)
{
if (!context.User.Identity.IsAuthenticated)
{
context.Fail();
return Task.CompletedTask;
}
var rankClaim = context.User.Claims.FirstOrDefault(c => c.Type == "Rank");
if (rankClaim != null && int.TryParse(rankClaim.Value, out int userRank))
{
// 判断用户职级是否达到最低要求
if (userRank >= requirement.MinimumRank)
{
context.Succeed(requirement);
}
else
{
context.Fail();
}
}
else
{
context.Fail();
}
return Task.CompletedTask;
}
}步骤 3:注册自定义策略 (Policy)#
回到 Startup.cs 的 ConfigureServices 方法,在 AddAuthorization 中注册我们的策略和处理程序。
public void ConfigureServices(IServiceCollection services)
{
// ... 之前的 JWT 认证配置 ...
services.AddAuthorization(options =>
{
// 注册一个名为 "HRDepartment" 的策略,它要求 Department 必须是 "HR"
options.AddPolicy("HRDepartment", policy =>
policy.Requirements.Add(new DepartmentRequirement("HR")));
// 注册一个名为 "ITDepartment" 的策略
options.AddPolicy("ITDepartment", policy =>
policy.Requirements.Add(new DepartmentRequirement("IT")));
// 注册一个名为 "ManagerOnly" 的策略,要求职级至少为 2 (Manager)
options.AddPolicy("ManagerOnly", policy =>
policy.Requirements.Add(new MinimumRankRequirement(2)));
// 组合策略:必须同时属于 IT 部门且是经理
options.AddPolicy("ITManager", policy =>
{
policy.Requirements.Add(new DepartmentRequirement("IT"));
policy.Requirements.Add(new MinimumRankRequirement(2));
});
});
// 将自定义的授权处理程序注册到依赖注入容器中,使其生命周期为 Singleton
services.AddSingleton<IAuthorizationHandler, DepartmentHandler>();
services.AddSingleton<IAuthorizationHandler, MinimumRankHandler>();
// ... 其他配置
}在控制器中使用授权#
使用 [Authorize(Policy = “...”)] 特性#
现在,我们可以在控制器或 Action 方法上使用 [Authorize] 特性来应用我们定义的策略。
[ApiController]
[Route("api/[controller]")]
[Authorize] // 整个控制器需要认证,但不对策略做要求
public class EmployeesController : ControllerBase
{
[HttpGet]
public IActionResult GetAllEmployees()
{
// 所有认证用户都可以访问
return Ok(new { message = "所有员工列表" });
}
[HttpGet("hr")]
[Authorize(Policy = "HRDepartment")] // 只有 HR 部门的用户能访问
public IActionResult GetHrEmployees()
{
return Ok(new { message = "HR部门员工列表" });
}
[HttpDelete("{id}")]
[Authorize(Policy = "ManagerOnly")] // 只有经理及以上职级的用户能删除
public IActionResult DeleteEmployee(int id)
{
return Ok(new { message = $"已删除员工 {id}" });
}
[HttpPost("it-bonus")]
[Authorize(Policy = "ITManager")] // 必须同时满足 IT 部门和经理职级
public IActionResult AssignItBonus()
{
return Ok(new { message = "IT 部门奖金已分配" });
}
}资源级授权#
有时授权决策需要依赖于正在被访问的特定资源。例如,“用户只能修改自己的个人资料”。这需要用到资源级授权。
-
创建资源要求:
public class SameUserRequirement : IAuthorizationRequirement { } -
创建资源处理程序:
public class SameUserHandler : AuthorizationHandler<SameUserRequirement, Employee> { protected override Task HandleRequirementAsync( AuthorizationHandlerContext context, SameUserRequirement requirement, Employee resource) { var userIdClaim = context.User.Claims.FirstOrDefault(c => c.Type == ClaimTypes.NameIdentifier); if (userIdClaim != null && resource.OwnerId == int.Parse(userIdClaim.Value)) { context.Succeed(requirement); } return Task.CompletedTask; } } -
在控制器中使用:
[HttpPut("{id}")] public IActionResult UpdateEmployee(int id, Employee model) { var employee = _repository.GetEmployee(id); // 授权检查:当前用户是否是该资源的拥有者? var authResult = await _authorizationService.AuthorizeAsync(User, employee, "SameUserPolicy"); if (authResult.Succeeded) { // 更新操作 return Ok(); } else { return Forbid(); } }
最佳实践与常见问题#
JWT 安全最佳实践#
- 保护密钥:JWT 签名密钥是最高机密,绝不要将其硬编码在代码中或提交到版本控制系统。使用 Azure Key Vault、HashiCorp Vault 或环境变量。
- 使用 HTTPS:在生产环境中,始终使用 HTTPS 来传输 JWT,防止中间人攻击。
- 设置合理的有效期:为令牌设置较短的有效期(如 15-60 分钟),并使用刷新令牌机制来获取新令牌。
- 验证所有参数:在
TokenValidationParameters中,确保ValidateIssuer、ValidateAudience等都为true。 - 避免在 JWT 中存储敏感数据:JWT 的 Payload 只是 Base64 编码,并非加密。不要存放密码等敏感信息。
策略设计建议#
- 保持要求和处理程序的单一职责:每个要求/处理程序只负责一个具体的检查点。
- 优先使用策略 over 角色:基于策略的授权比传统的基于角色的授权更灵活、更强大。
- 组合策略:通过组合简单的要求来构建复杂策略,提高代码复用性。
- 使用有意义的策略名称:策略名称应清晰表达其意图,如
"CanEditPost"比"Policy1"好得多。
常见问题排查#
- 返回 401 (Unauthorized) 而不是 403 (Forbidden):
- 401:表示认证失败,未提供有效的 JWT 或 JWT 已过期。
- 403:表示认证成功,但权限不足。
- 如果总是返回 401,请检查 JWT 认证中间件是否已正确配置和使用(
app.UseAuthentication()必须在app.UseAuthorization()和app.UseMvc()之前)。
- 策略不生效:检查处理程序是否已正确注册到 DI 容器(
services.AddSingleton<IAuthorizationHandler, MyHandler>())。 - 声明找不到:确认在生成 JWT 时已经将正确的声明(如
Department,Rank)添加到了 Token 中。
总结#
通过本文,我们系统地学习了如何在 ASP.NET Core 2.0 Web API 中结合 JWT 和自定义策略授权来构建一个安全、灵活且易于维护的权限系统。关键点在于理解认证(JWT) 和授权(策略) 的分离,以及如何通过定义要求、实现处理程序和注册策略这三个步骤来定制复杂的业务权限规则。
这种模式极大地提升了应用程序的安全性,并能优雅地适应未来需求的变化。