c#进阶疗法 -自定义鉴权

ASP.NET Core 自定义鉴权实战指南

什么是自定义鉴权?

自定义鉴权是指开发者根据应用程序的具体需求,自己实现的认证和授权逻辑,而不是使用框架提供的默认实现。在 ASP.NET Core 中,默认的认证和授权机制已经非常强大,但在某些复杂的场景下,可能需要更灵活、更细粒度的控制,这时候就需要使用自定义鉴权。

为什么需要自定义鉴权?

默认的认证和授权机制在以下场景下可能不够灵活:

  1. 复杂的权限规则:当权限规则非常复杂,无法通过简单的角色或声明来表达时,需要自定义鉴权。
  2. 动态权限管理:当权限规则需要在运行时动态修改,而不需要重新编译和部署应用程序时,需要自定义鉴权。
  3. 多维度权限控制:当权限控制需要考虑多个维度,如用户角色、用户属性、资源属性等时,需要自定义鉴权。
  4. 特殊的授权逻辑:当需要实现特殊的授权逻辑,如基于时间的授权、基于地理位置的授权等时,需要自定义鉴权。

如何实现自定义鉴权?

ASP.NET Core 中,实现自定义鉴权的方式有多种,如:

  1. 自定义中间件:通过实现自定义中间件来拦截请求并进行授权检查。
  2. 自定义授权过滤器:通过实现自定义授权过滤器来拦截请求并进行授权检查。
  3. 自定义授权策略:通过实现自定义授权策略来定义授权规则。
  4. 自定义授权处理程序:通过实现自定义授权处理程序来处理授权逻辑。

在本文中,我们将使用自定义中间件的方式来实现自定义鉴权,因为中间件可以在请求处理管道的早期拦截请求,提供更细粒度的控制。

自定义鉴权的具体实现

步骤 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 类,它包含 IdNameDescriptionPathPatternHttpMethodRequiredRolesRequiredClaimsAllowAnonymousPriorityIsActiveCreatedAtUpdatedAt 属性,用于定义权限规则。我们还创建了 ClaimRequirement 类,它包含 TypeValueOperator 属性,用于定义声明要求。最后,我们创建了 ClaimRequirementOperator 枚举,它定义了声明操作符,如 EqualNotEqualContainsStartsWithGreaterThanLessThan

步骤 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 接口,它定义了 GetAllActiveRulesGetRuleByIdAddOrUpdateRuleDeactivateRuleReloadFromConfiguration 方法,用于存储和管理权限规则。然后,我们创建了 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
    }
  ]
}

在这个示例中,我们配置了四个权限规则:

  1. AllowAnonymousHealthCheck:允许匿名访问 /health 路径,优先级为 10。
  2. AdminAPI:需要 Admin 角色才能访问 /api/admin/* 路径,优先级为 20。
  3. WeatherForecast:需要 UserAdmin 角色才能访问 /weatherforecast 路径,HTTP 方法为 GET,优先级为 30。
  4. 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 中,我们可以使用自定义中间件的方式来实现自定义鉴权。

通过本文的示例,我们学习了如何:

  1. 创建权限规则相关类,包括 PermissionRule 类、ClaimRequirement 类和 ClaimRequirementOperator 枚举。
  2. 创建权限存储接口和实现,包括 IPermissionStore 接口和 InMemoryPermissionStore 类。
  3. 实现自定义鉴权中间件,包括 DynamicAuthorizationMiddleware 类和 DynamicAuthorizationMiddlewareExtensions 类。
  4. 配置自定义鉴权,包括注册 IPermissionStore 服务和添加自定义鉴权中间件。
  5. 配置权限规则,包括在 appsettings.json 文件中配置权限规则。
  6. 测试自定义鉴权,包括测试获取 JWT 令牌、使用 JWT 令牌访问受保护的端点、测试未授权访问和测试角色不足。

我们还学习了自定义鉴权的最佳实践,包括安全存储权限规则、实现权限管理界面、缓存权限规则、实现权限审计、实现权限测试、使用 HTTPS、限制权限规则的复杂度,以及实现权限降级。

希望本文对你理解和使用 ASP.NET Core 自定义鉴权有所帮助!

相关推荐
FuckPatience2 小时前
C# .csproj Baseoutputpath/Outputpath、AppendTargetFrameworkToOutputPath
c#
初九之潜龙勿用2 小时前
C#实现导出Word图表通用方法之散点图
开发语言·c#·word·.net·office·图表
曹牧2 小时前
C#:WebReference
开发语言·c#
C#程序员一枚2 小时前
C#AsNoTracking()详解
开发语言·c#
明月看潮生3 小时前
编程与数学 03-008 《看潮企业管理软件》项目开发 01 需求分析 3-1
c#·.net·需求分析·erp·企业开发·项目实践·编程与数学
人工智能AI技术3 小时前
【C#程序员入门AI】环境一键搭建:.NET 8+AI开发环境(Semantic Kernel/ML.NET/ONNX Runtime)配置
人工智能·c#
CreasyChan4 小时前
unity 对象池实测可用
unity·c#
一个帅气昵称啊4 小时前
AI搜索增强C#实现多平台联网搜索并且将HTML内容转换为结构化的Markdown格式并整合内容输出结果
人工智能·c#·html
云草桑4 小时前
在C# .net中RabbitMQ的核心类型和属性,除了交换机,队列关键的类型 / 属性,影响其行为
c#·rabbitmq·.net·队列