ASP.NET Core 自定义鉴权实战指南
什么是自定义鉴权?
自定义鉴权是指开发者根据应用程序的具体需求,自己实现的认证和授权逻辑,而不是使用框架提供的默认实现。在 ASP.NET Core 中,默认的认证和授权机制已经非常强大,但在某些复杂的场景下,可能需要更灵活、更细粒度的控制,这时候就需要使用自定义鉴权。
为什么需要自定义鉴权?
默认的认证和授权机制在以下场景下可能不够灵活:
- 复杂的权限规则:当权限规则非常复杂,无法通过简单的角色或声明来表达时,需要自定义鉴权。
- 动态权限管理:当权限规则需要在运行时动态修改,而不需要重新编译和部署应用程序时,需要自定义鉴权。
- 多维度权限控制:当权限控制需要考虑多个维度,如用户角色、用户属性、资源属性等时,需要自定义鉴权。
- 特殊的授权逻辑:当需要实现特殊的授权逻辑,如基于时间的授权、基于地理位置的授权等时,需要自定义鉴权。
如何实现自定义鉴权?
在 ASP.NET Core 中,实现自定义鉴权的方式有多种,如:
- 自定义中间件:通过实现自定义中间件来拦截请求并进行授权检查。
- 自定义授权过滤器:通过实现自定义授权过滤器来拦截请求并进行授权检查。
- 自定义授权策略:通过实现自定义授权策略来定义授权规则。
- 自定义授权处理程序:通过实现自定义授权处理程序来处理授权逻辑。
在本文中,我们将使用自定义中间件的方式来实现自定义鉴权,因为中间件可以在请求处理管道的早期拦截请求,提供更细粒度的控制。
自定义鉴权的具体实现
步骤 1:创建权限规则相关类
首先,我们需要创建权限规则相关的类,包括 PermissionRule 类、ClaimRequirement 类和 ClaimRequirementOperator 枚举:
csharp
namespace FrameLearning.BasicComponents.Jwt.Permission
{
public class PermissionRule
{
public Guid Id { get; set; } = Guid.NewGuid();
public string Name { get; set; } // 规则名称,如 "AdminAccess"
public string Description { get; set; }
public string PathPattern { get; set; } // 路径模式,支持通配符 /api/admin/*
public string HttpMethod { get; set; } = "*"; // GET, POST, PUT, DELETE, *
public List<string> RequiredRoles { get; set; } = new List<string>();
public List<ClaimRequirement> RequiredClaims { get; set; } = new List<ClaimRequirement>();
public bool AllowAnonymous { get; set; } // 是否允许匿名访问
public int Priority { get; set; } = 100; // 优先级,数值越小优先级越高
public bool IsActive { get; set; } = true;
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public DateTime? UpdatedAt { get; set; }
}
// 声明要求
public class ClaimRequirement
{
public string Type { get; set; }
public string Value { get; set; }
public ClaimRequirementOperator Operator { get; set; } = ClaimRequirementOperator.Equal;
}
// 声明操作符
public enum ClaimRequirementOperator
{
Equal,
NotEqual,
Contains,
StartsWith,
GreaterThan,
LessThan
}
}
在这个示例中,我们创建了 PermissionRule 类,它包含 Id、Name、Description、PathPattern、HttpMethod、RequiredRoles、RequiredClaims、AllowAnonymous、Priority、IsActive、CreatedAt 和 UpdatedAt 属性,用于定义权限规则。我们还创建了 ClaimRequirement 类,它包含 Type、Value 和 Operator 属性,用于定义声明要求。最后,我们创建了 ClaimRequirementOperator 枚举,它定义了声明操作符,如 Equal、NotEqual、Contains、StartsWith、GreaterThan 和 LessThan。
步骤 2:创建权限存储接口和实现
接下来,我们需要创建权限存储接口和实现,用于存储和管理权限规则:
csharp
namespace FrameLearning.BasicComponents.Jwt.Permission
{
// 内存权限存储(线程安全)
public interface IPermissionStore
{
List<PermissionRule> GetAllActiveRules();
PermissionRule GetRuleById(Guid id);
void AddOrUpdateRule(PermissionRule rule);
void DeactivateRule(Guid id);
void ReloadFromConfiguration(IConfiguration configuration);
}
public class InMemoryPermissionStore : IPermissionStore
{
private readonly object _lock = new object();
private List<PermissionRule> _rules = new List<PermissionRule>();
private readonly ILogger<InMemoryPermissionStore> _logger;
private readonly IConfiguration _configuration;
public InMemoryPermissionStore(
ILogger<InMemoryPermissionStore> logger,
IConfiguration configuration)
{
_logger = logger;
_configuration = configuration;
InitializeDefaultRules();
}
/// <summary>
/// 初始化虚拟权限,项目数据需要添加到数据库中
/// </summary>
private void InitializeDefaultRules()
{
try
{
// 从appsettings.json加载初始规则
ReloadFromConfiguration(_configuration);
_logger.LogInformation("已从配置加载 {Count} 条权限规则", _rules.Count);
}
catch (Exception ex)
{
_logger.LogError(ex, "初始化默认权限规则失败,使用内置默认规则");
// 内置默认规则
_rules = new List<PermissionRule>
{
new PermissionRule
{
Name = "AllowAnonymousHealthCheck",
PathPattern = "/health",
HttpMethod = "*",
AllowAnonymous = true,
Priority = 10
},
new PermissionRule
{
Name = "AdminAPI",
PathPattern = "/api/admin/*",
HttpMethod = "*",
RequiredRoles = new List<string> { "Admin" },
Priority = 20
},
new PermissionRule
{
Name = "WeatherForecast",
PathPattern = "/weatherforecast",
HttpMethod = "GET",
RequiredRoles = new List<string> { "User", "Admin" },
Priority = 30
},
new PermissionRule
{
Name = "DefaultDeny",
PathPattern = "*",
HttpMethod = "*",
AllowAnonymous = false,
Priority = 1000 // 最低优先级,作为兜底规则
}
};
}
}
public List<PermissionRule> GetAllActiveRules()
{
lock (_lock)
{
return _rules.Where(r => r.IsActive).OrderBy(r => r.Priority).ToList();
}
}
public PermissionRule GetRuleById(Guid id)
{
lock (_lock)
{
return _rules.FirstOrDefault(r => r.Id == id);
}
}
public void AddOrUpdateRule(PermissionRule rule)
{
lock (_lock)
{
var existing = _rules.FirstOrDefault(r => r.Id == rule.Id);
if (existing != null)
{
// 更新现有规则
existing.Name = rule.Name;
existing.Description = rule.Description;
existing.PathPattern = rule.PathPattern;
existing.HttpMethod = rule.HttpMethod;
existing.RequiredRoles = rule.RequiredRoles;
existing.RequiredClaims = rule.RequiredClaims;
existing.AllowAnonymous = rule.AllowAnonymous;
existing.Priority = rule.Priority;
existing.IsActive = rule.IsActive;
existing.UpdatedAt = DateTime.UtcNow;
}
else
{
// 添加新规则
rule.Id = Guid.NewGuid();
_rules.Add(rule);
}
// 重新排序
_rules = _rules.OrderBy(r => r.Priority).ToList();
}
_logger.LogInformation("权限规则已更新: {RuleName}", rule.Name);
}
public void DeactivateRule(Guid id)
{
lock (_lock)
{
var rule = _rules.FirstOrDefault(r => r.Id == id);
if (rule != null)
{
rule.IsActive = false;
rule.UpdatedAt = DateTime.UtcNow;
}
}
}
public void ReloadFromConfiguration(IConfiguration configuration)
{
lock (_lock)
{
var rulesConfig = configuration.GetSection("PermissionRules").Get<List<PermissionRule>>()
?? new List<PermissionRule>();
_rules = rulesConfig.Select(r =>
{
r.Id = Guid.NewGuid(); // 重置ID
r.CreatedAt = DateTime.UtcNow;
return r;
}).OrderBy(r => r.Priority).ToList();
}
_logger.LogInformation("权限规则已从配置重载,共 {Count} 条规则", _rules.Count);
}
}
}
在这个示例中,我们创建了 IPermissionStore 接口,它定义了 GetAllActiveRules、GetRuleById、AddOrUpdateRule、DeactivateRule 和 ReloadFromConfiguration 方法,用于存储和管理权限规则。然后,我们创建了 InMemoryPermissionStore 类,它实现了 IPermissionStore 接口的方法。在 InitializeDefaultRules 方法中,我们尝试从 appsettings.json 加载初始规则,如果失败则使用内置默认规则。在 GetAllActiveRules 方法中,我们返回所有激活的规则,并按优先级排序。在 GetRuleById 方法中,我们根据 ID 获取规则。在 AddOrUpdateRule 方法中,我们添加或更新规则。在 DeactivateRule 方法中,我们停用规则。在 ReloadFromConfiguration 方法中,我们从配置重载规则。
步骤 3:实现自定义鉴权中间件
现在,我们需要实现自定义鉴权中间件,用于拦截请求并进行授权检查:
csharp
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc.Controllers;
using System.Reflection;
using System.Security.Claims;
using System.Text.Json;
using System.Text.RegularExpressions;
namespace FrameLearning.BasicComponents.Jwt.Permission
{
// 全局动态授权中间件
public class DynamicAuthorizationMiddleware
{
private readonly RequestDelegate _next;
private readonly IPermissionStore _permissionStore;
private readonly ILogger<DynamicAuthorizationMiddleware> _logger;
public DynamicAuthorizationMiddleware(
RequestDelegate next,
IPermissionStore permissionStore,
ILogger<DynamicAuthorizationMiddleware> logger)
{
_next = next;
_permissionStore = permissionStore;
_logger = logger;
}
public async Task InvokeAsync(HttpContext context)
{
if (await AuthorizeRequestAsync(context))
{
await _next(context);
}
}
private async Task<bool> AuthorizeRequestAsync(HttpContext context)
{
var path = context.Request.Path.Value?.ToLowerInvariant() ?? "/";
var method = context.Request.Method.ToUpperInvariant();
// ========== 全局生效:优先判断是否允许匿名 ==========
if (IsRequestAllowAnonymous(context))
{
_logger.LogInformation("当前请求允许匿名访问,跳过授权校验: {Path} {Method}",
context.Request.Path, context.Request.Method);
return true;
}
var rules = _permissionStore.GetAllActiveRules();
// 按优先级排序,找到第一个匹配的规则
foreach (var rule in rules.OrderBy(r => r.Priority))
{
if (!IsPathMatch(path, rule.PathPattern.ToLowerInvariant()) ||
!IsMethodMatch(method, rule.HttpMethod.ToUpperInvariant()))
{
continue;
}
_logger.LogDebug("匹配规则: {RuleName} - {PathPattern} {HttpMethod}",
rule.Name, rule.PathPattern, rule.HttpMethod);
// 允许匿名访问
if (rule.AllowAnonymous)
{
_logger.LogDebug("规则 '{RuleName}' 允许匿名访问 {Path} {Method}", rule.Name, path, method);
return true;
}
// 检查用户是否已认证
if (!context.User.Identity?.IsAuthenticated == true)
{
_logger.LogWarning("401 未授权访问: {Path} {Method}", path, method);
await HandleUnauthorizedAsync(context);
return false;
}
// 检查角色
if (rule.RequiredRoles.Any())//rule.RequiredRoles 访问资源所需要的角色
{
bool hasRequiredRole = rule.RequiredRoles.Any(role =>
context.User.IsInRole(role));//role:当前迭代的角色名称【context.User.IsInRole(role):ASP.NET Core内置方法,检查当前用户是否属于指定角色】
if (!hasRequiredRole)
{
var userRoles = context.User.Claims
.Where(c => c.Type == ClaimTypes.Role)
.Select(c => c.Value)
.ToList();
_logger.LogWarning("403 角色不足: 用户 '{UserName}' (角色: {UserRoles}) 尝试访问 {Path} {Method},需要角色: {RequiredRoles}",
context.User.Identity.Name, string.Join(", ", userRoles), path, method, string.Join(", ", rule.RequiredRoles));
await HandleForbiddenAsync(context, rule, "角色不足");
return false;
}
}
// 检查声明
foreach (var claimReq in rule.RequiredClaims)
{
var userClaim = context.User.FindFirst(claimReq.Type);
if (userClaim == null)
{
_logger.LogWarning("403 声明缺失: 用户 '{UserName}' 缺少声明 '{ClaimType}' 访问 {Path} {Method}",
context.User.Identity.Name, claimReq.Type, path, method);
await HandleForbiddenAsync(context, rule, $"缺少声明: {claimReq.Type}");
return false;
}
bool claimMatches = claimReq.Operator switch
{
ClaimRequirementOperator.Equal => userClaim.Value.Equals(claimReq.Value, StringComparison.OrdinalIgnoreCase),
ClaimRequirementOperator.NotEqual => !userClaim.Value.Equals(claimReq.Value, StringComparison.OrdinalIgnoreCase),
ClaimRequirementOperator.Contains => userClaim.Value.Contains(claimReq.Value, StringComparison.OrdinalIgnoreCase),
ClaimRequirementOperator.StartsWith => userClaim.Value.StartsWith(claimReq.Value, StringComparison.OrdinalIgnoreCase),
ClaimRequirementOperator.GreaterThan => string.Compare(userClaim.Value, claimReq.Value, StringComparison.OrdinalIgnoreCase) > 0,
ClaimRequirementOperator.LessThan => string.Compare(userClaim.Value, claimReq.Value, StringComparison.OrdinalIgnoreCase) < 0,
_ => userClaim.Value.Equals(claimReq.Value, StringComparison.OrdinalIgnoreCase)
};
if (!claimMatches)
{
_logger.LogWarning("403 声明不匹配: 用户 '{UserName}' 声明 '{ClaimType}' 值 '{UserValue}' 不匹配要求 '{RequiredValue}' (操作符: {Operator})",
context.User.Identity.Name, claimReq.Type, userClaim.Value, claimReq.Value, claimReq.Operator);
await HandleForbiddenAsync(context, rule, $"声明不匹配: {claimReq.Type}");
return false;
}
}
// 所有条件都满足
_logger.LogDebug("规则 '{RuleName}' 授权用户 '{UserName}' 访问 {Path} {Method}",
rule.Name, context.User.Identity.Name, path, method);
return true;
}
// 没有匹配的规则,拒绝访问
_logger.LogWarning("403 无匹配规则: {Path} {Method}", path, method);
await HandleForbiddenAsync(context, null, "无访问权限");
return false;
}
/// <summary>
/// 通用方法:判断当前请求是否允许匿名访问(适配所有接口类型)
/// </summary>
/// <param name="context">HttpContext</param>
/// <returns>是否允许匿名</returns>
private bool IsRequestAllowAnonymous(HttpContext context)
{
var endpoint = context.GetEndpoint();
if (endpoint == null)
{
_logger.LogWarning("Endpoint为空 | 路径:{Path} | 请检查路由配置", context.Request.Path);
return false;
}
// 修复:全覆盖检测 AllowAnonymousAttribute(包括接口/特性继承)
var hasAllowAnonymous = endpoint.Metadata.Any(meta =>
meta is AllowAnonymousAttribute ||
(meta.GetType().FullName == "Microsoft.AspNetCore.Authorization.AllowAnonymousAttribute"));
if (hasAllowAnonymous)
{
_logger.LogDebug("检测到[AllowAnonymous]特性,允许匿名访问: {Path}", context.Request.Path);
return true;
}
// 兜底:兼容老版本框架的元数据格式
var controllerActionDescriptor = endpoint.Metadata.GetMetadata<ControllerActionDescriptor>();
if (controllerActionDescriptor != null)
{
// 检查Action特性(修复:使用GetCustomAttribute而非Any,避免漏检)
var actionAttr = controllerActionDescriptor.MethodInfo
.GetCustomAttribute<AllowAnonymousAttribute>(inherit: true);
// 检查Controller特性
var controllerAttr = controllerActionDescriptor.ControllerTypeInfo
.GetCustomAttribute<AllowAnonymousAttribute>(inherit: true);
if (actionAttr != null || controllerAttr != null)
{
_logger.LogDebug("通过反射检测到[AllowAnonymous] | Action:{Action}",
controllerActionDescriptor.ActionName);
return true;
}
}
return false;
}
private bool IsPathMatch(string requestPath, string pattern)
{
if (string.Equals(pattern, "*", StringComparison.OrdinalIgnoreCase))
return true;
// 支持通配符 *
if (pattern.Contains('*'))
{
var regexPattern = "^" + Regex.Escape(pattern).Replace("\\*", ".*") + "$";
return Regex.IsMatch(requestPath, regexPattern, RegexOptions.IgnoreCase);
}
// 支持结尾通配符 /
if (pattern.EndsWith("/*", StringComparison.OrdinalIgnoreCase))
{
var basePath = pattern.Substring(0, pattern.Length - 1); // 移除最后的 *
return requestPath.StartsWith(basePath, StringComparison.OrdinalIgnoreCase);
}
// 精确匹配
return string.Equals(requestPath, pattern, StringComparison.OrdinalIgnoreCase);
}
private bool IsMethodMatch(string requestMethod, string ruleMethod)
{
return ruleMethod == "*" || string.Equals(requestMethod, ruleMethod, StringComparison.OrdinalIgnoreCase);
}
private async Task HandleUnauthorizedAsync(HttpContext context)
{
context.Response.StatusCode = StatusCodes.Status401Unauthorized;
context.Response.ContentType = "application/json";
var response = new
{
code = 401,
message = "未授权访问",
detail = "请提供有效的身份验证凭证",
path = context.Request.Path,
timestamp = DateTime.UtcNow
};
await context.Response.WriteAsync(JsonSerializer.Serialize(response, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }));
}
private async Task HandleForbiddenAsync(HttpContext context, PermissionRule rule, string reason)
{
context.Response.StatusCode = StatusCodes.Status403Forbidden;
context.Response.ContentType = "application/json";
var response = new
{
code = 403,
message = "禁止访问",
detail = reason,
requiredRoles = rule?.RequiredRoles ?? new List<string>(),
requiredClaims = rule?.RequiredClaims?.Select(c => new { c.Type, c.Value, c.Operator }),
yourRoles = context.User.Claims
.Where(c => c.Type == ClaimTypes.Role)
.Select(c => c.Value)
.ToList(),
yourClaims = context.User.Claims
.Select(c => new { c.Type, c.Value })
.ToList(),
path = context.Request.Path,
timestamp = DateTime.UtcNow
};
await context.Response.WriteAsync(JsonSerializer.Serialize(response, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }));
}
}
// 中间件扩展方法
public static class DynamicAuthorizationMiddlewareExtensions
{
public static IApplicationBuilder UseDynamicAuthorization(this IApplicationBuilder builder)
{
return builder.UseMiddleware<DynamicAuthorizationMiddleware>();
}
}
}
在这个示例中,我们创建了 DynamicAuthorizationMiddleware 类,它实现了 InvokeAsync 方法,用于拦截请求并进行授权检查。在 AuthorizeRequestAsync 方法中,我们首先检查请求是否允许匿名访问,如果允许则跳过授权校验。然后,我们获取所有激活的规则,并按优先级排序,找到第一个匹配的规则。对于匹配的规则,我们检查是否允许匿名访问,如果允许则返回 true。如果不允许匿名访问,则检查用户是否已认证,如果未认证则返回 401 未授权访问。如果用户已认证,则检查角色是否满足要求,如果不满足则返回 403 角色不足。如果角色满足要求,则检查声明是否满足要求,如果不满足则返回 403 声明不匹配。如果所有条件都满足,则返回 true。如果没有匹配的规则,则返回 403 无访问权限。我们还创建了 IsRequestAllowAnonymous 方法,用于判断当前请求是否允许匿名访问。我们还创建了 IsPathMatch 方法,用于判断请求路径是否匹配规则路径模式。我们还创建了 IsMethodMatch 方法,用于判断请求方法是否匹配规则 HTTP 方法。我们还创建了 HandleUnauthorizedAsync 方法,用于处理未授权访问。我们还创建了 HandleForbiddenAsync 方法,用于处理禁止访问。最后,我们创建了 DynamicAuthorizationMiddlewareExtensions 类,它提供了 UseDynamicAuthorization 扩展方法,用于注册中间件。
步骤 4:配置自定义鉴权
在 Program.cs 文件中,我们需要配置自定义鉴权:
csharp
// 注册权限存储
builder.Services.AddSingleton<IPermissionStore, InMemoryPermissionStore>();
// 添加自定义鉴权中间件
app.UseDynamicAuthorization();
在这个示例中,我们首先注册了 IPermissionStore 服务,使用 InMemoryPermissionStore 作为实现。然后,我们添加了自定义鉴权中间件。
步骤 5:配置权限规则
在 appsettings.json 文件中,我们可以配置权限规则:
json
{
"PermissionRules": [
{
"Name": "AllowAnonymousHealthCheck",
"PathPattern": "/health",
"HttpMethod": "*",
"AllowAnonymous": true,
"Priority": 10
},
{
"Name": "AdminAPI",
"PathPattern": "/api/admin/*",
"HttpMethod": "*",
"RequiredRoles": ["Admin"],
"Priority": 20
},
{
"Name": "WeatherForecast",
"PathPattern": "/weatherforecast",
"HttpMethod": "GET",
"RequiredRoles": ["User", "Admin"],
"Priority": 30
},
{
"Name": "DefaultDeny",
"PathPattern": "*",
"HttpMethod": "*",
"AllowAnonymous": false,
"Priority": 1000
}
]
}
在这个示例中,我们配置了四个权限规则:
AllowAnonymousHealthCheck:允许匿名访问/health路径,优先级为 10。AdminAPI:需要Admin角色才能访问/api/admin/*路径,优先级为 20。WeatherForecast:需要User或Admin角色才能访问/weatherforecast路径,HTTP 方法为GET,优先级为 30。DefaultDeny:默认拒绝访问所有路径,优先级为 1000。
测试自定义鉴权
步骤 1:获取 JWT 令牌
首先,我们需要发送一个 POST 请求到登录端点,获取 JWT 令牌:
http
POST /weatherforecast/login
Content-Type: application/json
{
"username": "John",
"password": "126.com"
}
响应应该类似于:
json
{
"username": "John",
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1lIjoiSm9obiIsImp0aSI6ImI3MDExYjZlLTZhZTQtNDgxMS05MGFlLWQxMDBkNjA5M2JmOSIsImh0dHA6Ly9zY2hlbWFzLm1pY3Jvc29mdC5jb20vd3MvMjAwOC8wNi9pZGVudGl0eS9jbGFpbXMvcm9sZSI6IkFkbWluIiwiZXhwIjoxNzY4OTAwOTc4LCJpc3MiOiJUZXN0QXBpIiwiYXVkIjoiVGVzdEFwaVVzZXJzIn0.nggbL8AmiYEejRAFZzCX6Xhv5dAuNGfjy45hQ7d225o",
"roles": [
"Admin"
]
}
步骤 2:使用 JWT 令牌访问受保护的端点
然后,我们可以使用获取到的 JWT 令牌访问受保护的端点:
http
GET /weatherforecast
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1lIjoiSm9obiIsImp0aSI6ImI3MDExYjZlLTZhZTQtNDgxMS05MGFlLWQxMDBkNjA5M2JmOSIsImh0dHA6Ly9zY2hlbWFzLm1pY3Jvc29mdC5jb20vd3MvMjAwOC8wNi9pZGVudGl0eS9jbGFpbXMvcm9sZSI6IkFkbWluIiwiZXhwIjoxNzY4OTAwOTc4LCJpc3MiOiJUZXN0QXBpIiwiYXVkIjoiVGVzdEFwaVVzZXJzIn0.nggbL8AmiYEejRAFZzCX6Xhv5dAuNGfjy45hQ7d225o
响应应该类似于:
json
[
{
"date": "2026-01-22",
"temperatureC": 12,
"temperatureF": 53,
"summary": "Mild"
},
{
"date": "2026-01-23",
"temperatureC": 23,
"temperatureF": 73,
"summary": "Warm"
},
{
"date": "2026-01-24",
"temperatureC": -5,
"temperatureF": 23,
"summary": "Freezing"
},
{
"date": "2026-01-25",
"temperatureC": 34,
"temperatureF": 93,
"summary": "Hot"
},
{
"date": "2026-01-26",
"temperatureC": 18,
"temperatureF": 64,
"summary": "Balmy"
}
]
步骤 3:测试未授权访问
如果我们不提供 JWT 令牌或者提供的令牌无效,访问受保护的端点应该返回未授权响应:
http
GET /weatherforecast
响应应该类似于:
json
{
"code": 401,
"message": "未授权访问",
"detail": "请提供有效的身份验证凭证",
"path": "/weatherforecast",
"timestamp": "2026-01-21T00:00:00Z"
}
步骤 4:测试角色不足
如果我们使用一个没有足够角色的用户的令牌访问受保护的端点,应该返回角色不足响应:
http
GET /api/admin/users
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1lIjoiSm9obiIsImp0aSI6ImI3MDExYjZlLTZhZTQtNDgxMS05MGFlLWQxMDBkNjA5M2JmOSIsImh0dHA6Ly9zY2hlbWFzLm1pY3Jvc29mdC5jb20vd3MvMjAwOC8wNi9pZGVudGl0eS9jbGFpbXMvcm9sZSI6IlVzZXIiLCJleHAiOjE3Njg5MDA5NzgsImlzcyI6IlRlc3RBcGkiLCJhdWQiOiJUZXN0QXBpVXNlcnMifQ.7d2Q5Y8Z9X0A1B2C3D4E5F6G7H8I9J0K1L2M3N4O5P6Q7R8S9T0U1V2W3X4Y5Z6
响应应该类似于:
json
{
"code": 403,
"message": "禁止访问",
"detail": "角色不足",
"requiredRoles": ["Admin"],
"requiredClaims": [],
"yourRoles": ["User"],
"yourClaims": [
{
"type": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name",
"value": "John"
},
{
"type": "http://schemas.microsoft.com/ws/2008/06/identity/claims/role",
"value": "User"
}
],
"path": "/api/admin/users",
"timestamp": "2026-01-21T00:00:00Z"
}
自定义鉴权的最佳实践
1. 安全存储权限规则
权限规则应该安全存储,不应该硬编码在代码中或者存储在版本控制系统中。建议使用数据库或配置服务来存储权限规则。
2. 实现权限管理界面
为了方便管理权限规则,建议实现一个权限管理界面,允许管理员在运行时动态修改权限规则。
3. 缓存权限规则
为了提高性能,建议缓存权限规则,避免每次请求都从存储中获取权限规则。
4. 实现权限审计
为了提高安全性,建议实现权限审计,记录权限规则的修改历史和权限检查的结果。
5. 实现权限测试
为了确保权限规则的正确性,建议实现权限测试,验证权限规则的行为是否符合预期。
6. 使用 HTTPS
权限规则应该通过 HTTPS 传输,以防止中间人攻击。
7. 限制权限规则的复杂度
权限规则应该保持简洁明了,避免过于复杂的规则,以提高可维护性和性能。
8. 实现权限降级
为了提高系统的可用性,建议实现权限降级,当权限存储不可用时,使用默认权限规则。
总结
自定义鉴权是指开发者根据应用程序的具体需求,自己实现的认证和授权逻辑,而不是使用框架提供的默认实现。在 ASP.NET Core 中,我们可以使用自定义中间件的方式来实现自定义鉴权。
通过本文的示例,我们学习了如何:
- 创建权限规则相关类,包括
PermissionRule类、ClaimRequirement类和ClaimRequirementOperator枚举。 - 创建权限存储接口和实现,包括
IPermissionStore接口和InMemoryPermissionStore类。 - 实现自定义鉴权中间件,包括
DynamicAuthorizationMiddleware类和DynamicAuthorizationMiddlewareExtensions类。 - 配置自定义鉴权,包括注册
IPermissionStore服务和添加自定义鉴权中间件。 - 配置权限规则,包括在
appsettings.json文件中配置权限规则。 - 测试自定义鉴权,包括测试获取 JWT 令牌、使用 JWT 令牌访问受保护的端点、测试未授权访问和测试角色不足。
我们还学习了自定义鉴权的最佳实践,包括安全存储权限规则、实现权限管理界面、缓存权限规则、实现权限审计、实现权限测试、使用 HTTPS、限制权限规则的复杂度,以及实现权限降级。
希望本文对你理解和使用 ASP.NET Core 自定义鉴权有所帮助!