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 认证,实现灵活且强大的自定义策略授权。

目录#

  1. 核心概念解析
    1. JWT (JSON Web Token)
    2. 认证 (Authentication) 与授权 (Authorization)
    3. 策略 (Policy) 与要求 (Requirement)
  2. 项目搭建与基础配置
    1. 创建项目与引入依赖
    2. 配置 JWT 认证服务
  3. 实现自定义策略授权
    1. 场景定义:基于部门与职级的权限控制
    2. 步骤 1:定义自定义授权要求 (Requirement)
    3. 步骤 2:实现授权处理程序 (Authorization Handler)
    4. 步骤 3:注册自定义策略 (Policy)
  4. 在控制器中使用授权
    1. 使用 [Authorize(Policy = “...”)] 特性
    2. 资源级授权
  5. 最佳实践与常见问题
    1. JWT 安全最佳实践
    2. 策略设计建议
    3. 常见问题排查
  6. 总结
  7. 参考

核心概念解析#

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"。
  • 要求:策略的基本组成部分,代表授权必须满足的一个条件。处理程序负责评估这些要求。
  • 处理程序:负责检查要求是否得到满足。一个要求可以有多个处理程序。

项目搭建与基础配置#

创建项目与引入依赖#

  1. 使用 Visual Studio 或 CLI 创建一个新的 ASP.NET Core Web API 项目。
  2. 通过 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 部门奖金已分配" });
    }
}

资源级授权#

有时授权决策需要依赖于正在被访问的特定资源。例如,“用户只能修改自己的个人资料”。这需要用到资源级授权。

  1. 创建资源要求:

    public class SameUserRequirement : IAuthorizationRequirement { }
  2. 创建资源处理程序:

    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;
        }
    }
  3. 在控制器中使用:

    [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 安全最佳实践#

  1. 保护密钥:JWT 签名密钥是最高机密,绝不要将其硬编码在代码中或提交到版本控制系统。使用 Azure Key Vault、HashiCorp Vault 或环境变量。
  2. 使用 HTTPS:在生产环境中,始终使用 HTTPS 来传输 JWT,防止中间人攻击。
  3. 设置合理的有效期:为令牌设置较短的有效期(如 15-60 分钟),并使用刷新令牌机制来获取新令牌。
  4. 验证所有参数:在 TokenValidationParameters 中,确保 ValidateIssuer、ValidateAudience 等都为 true。
  5. 避免在 JWT 中存储敏感数据:JWT 的 Payload 只是 Base64 编码,并非加密。不要存放密码等敏感信息。

策略设计建议#

  1. 保持要求和处理程序的单一职责:每个要求/处理程序只负责一个具体的检查点。
  2. 优先使用策略 over 角色:基于策略的授权比传统的基于角色的授权更灵活、更强大。
  3. 组合策略:通过组合简单的要求来构建复杂策略,提高代码复用性。
  4. 使用有意义的策略名称:策略名称应清晰表达其意图,如 "CanEditPost" 比 "Policy1" 好得多。

常见问题排查#

  1. 返回 401 (Unauthorized) 而不是 403 (Forbidden):
    • 401:表示认证失败,未提供有效的 JWT 或 JWT 已过期。
    • 403:表示认证成功,但权限不足。
    • 如果总是返回 401,请检查 JWT 认证中间件是否已正确配置和使用(app.UseAuthentication() 必须在 app.UseAuthorization() 和 app.UseMvc() 之前)。
  2. 策略不生效:检查处理程序是否已正确注册到 DI 容器(services.AddSingleton<IAuthorizationHandler, MyHandler>())。
  3. 声明找不到:确认在生成 JWT 时已经将正确的声明(如 Department, Rank)添加到了 Token 中。

总结#

通过本文,我们系统地学习了如何在 ASP.NET Core 2.0 Web API 中结合 JWT 和自定义策略授权来构建一个安全、灵活且易于维护的权限系统。关键点在于理解认证(JWT) 和授权(策略) 的分离,以及如何通过定义要求、实现处理程序和注册策略这三个步骤来定制复杂的业务权限规则。

这种模式极大地提升了应用程序的安全性,并能优雅地适应未来需求的变化。

参考#

  1. ASP.NET Core 官方文档 - 授权
  2. ASP.NET Core 官方文档 - 基于策略的授权
  3. JSON Web Tokens Introduction - jwt.io
  4. RFC 7519 - JSON Web Token (JWT)