【问题标题】:How do you create a custom AuthorizeAttribute in ASP.NET Core?如何在 ASP.NET Core 中创建自定义 AuthorizeAttribute?
【发布时间】:2015-10-06 11:57:19
【问题描述】:

我正在尝试在 ASP.NET Core 中创建自定义授权属性。在以前的版本中,可以覆盖bool AuthorizeCore(HttpContextBase httpContext)。但这在AuthorizeAttribute 中不再存在。

目前制作自定义 AuthorizeAttribute 的方法是什么?

我要完成的工作:我在标头授权中收到一个会话 ID。通过该 ID,我将知道特定操作是否有效。

【问题讨论】:

  • 我不知道该怎么做,但 MVC 是开源的。您可以拉取 github 存储库并查找 IAuthorizationFilter 的实现。如果我今天有时间,我会找你并发布一个实际的答案,但没有承诺。 github 仓库:github.com/aspnet/Mvc
  • 好的,没时间了,但是在使用 AuthorizeAttribute 的 MVC 存储库中查找 AuthorizationPolicy 的用法,在 aspnet/Security 存储库中,这里:github.com/aspnet/Security。或者,在 MVC 存储库中查找您关心的安全内容似乎所在的命名空间,即 Microsoft.AspNet.Authorization。抱歉,我无法提供更多帮助。祝你好运!

标签: c# asp.net-core asp.net-core-mvc authorization


【解决方案1】:

ASP.Net Core 团队推荐的方法是使用完整记录的新策略设计here。新方法背后的基本思想是使用新的[Authorize] 属性来指定一个“策略”(例如[Authorize( Policy = "YouNeedToBe18ToDoThis")],其中该策略在应用程序的Startup.cs 中注册以执行一些代码块(即确保用户拥有年龄为 18 岁或以上的年龄声明)。

策略设计是对框架的一个很好的补充,应该赞扬 ASP.Net 安全核心团队的引入。也就是说,它并不适合所有情况。这种方法的缺点是它无法为简单地断言给定控制器或动作需要给定声明类型的最常见需求提供方便的解决方案。在应用程序可能具有数百个离散权限管理单个 REST 资源(“CanCreateOrder”、“CanReadOrder”、“CanUpdateOrder”、“CanDeleteOrder”等)上的离散权限的情况下,新方法要么需要重复的一对一策略名称和声明名称之间的一个映射(例如options.AddPolicy("CanUpdateOrder", policy => policy.RequireClaim(MyClaimTypes.Permission, "CanUpdateOrder));),或者编写一些代码以在运行时执行这些注册(例如从数据库中读取所有声明类型并在循环中执行上述调用)。在大多数情况下,这种方法的问题在于它是不必要的开销。

虽然 ASP.Net Core 安全团队建议永远不要创建自己的解决方案,但在某些情况下,这可能是最谨慎的选择。

以下是一个实现,它使用IAuthorizationFilter 提供一种简单的方式来表达给定控制器或操作的声明要求:

public class ClaimRequirementAttribute : TypeFilterAttribute
{
    public ClaimRequirementAttribute(string claimType, string claimValue) : base(typeof(ClaimRequirementFilter))
    {
        Arguments = new object[] {new Claim(claimType, claimValue) };
    }
}

public class ClaimRequirementFilter : IAuthorizationFilter
{
    readonly Claim _claim;

    public ClaimRequirementFilter(Claim claim)
    {
        _claim = claim;
    }

    public void OnAuthorization(AuthorizationFilterContext context)
    {
        var hasClaim = context.HttpContext.User.Claims.Any(c => c.Type == _claim.Type && c.Value == _claim.Value);
        if (!hasClaim)
        {
            context.Result = new ForbidResult();
        }
    }
}


[Route("api/resource")]
public class MyController : Controller
{
    [ClaimRequirement(MyClaimTypes.Permission, "CanReadResource")]
    [HttpGet]
    public IActionResult GetResource()
    {
        return Ok();
    }
}

【讨论】:

  • 如何注册 ClaimRequirementFilter?是自动处理的吗?
  • 奇怪的是有人从这里的所有答案中删除了 cmets。无论如何,不​​需要注册。框架通过扩展 TypeFilterAttribute 自动调用过滤器。
  • 这太棒了!在控制器中工作。如何签入 Razor 页面?
【解决方案2】:

我是 asp.net 安全人员。 首先让我很抱歉,除了音乐商店示例或单元测试之外,还没有任何文档记录,并且在公开的 API 方面仍在完善。 详细文档是 here

我们不希望您编写自定义授权属性。如果你需要这样做,我们做错了什么。相反,您应该编写授权要求

授权作用于身份。身份是通过身份验证创建的。

您在 cmets 中说您要检查标头中的会话 ID。您的会话 ID 将是身份的基础。如果您想使用Authorize 属性,您将编写一个身份验证中间件来获取该标头并将其转换为经过身份验证的ClaimsPrincipal。然后,您将在授权要求中进行检查。授权要求可以随心所欲,例如,这里的要求是对当前身份进行出生日期声明,如果用户超过 18 岁就会授权;

public class Over18Requirement : AuthorizationHandler<Over18Requirement>, IAuthorizationRequirement
{
  public override void Handle(AuthorizationHandlerContext context, Over18Requirement requirement)
  {
    if (!context.User.HasClaim(c => c.Type == ClaimTypes.DateOfBirth))
    {
      context.Fail();
      return;
    }

    var dobVal = context.User.FindFirst(c => c.Type == ClaimTypes.DateOfBirth).Value;
    var dateOfBirth = Convert.ToDateTime(dobVal);
    int age = DateTime.Today.Year - dateOfBirth.Year;
    if (dateOfBirth > DateTime.Today.AddYears(-age))
    {
      age--;
    }

    if (age >= 18)
    {
      context.Succeed(requirement);
    }
    else
    {
      context.Fail();
    }
  }
}

然后在您的 ConfigureServices() 函数中将其连接起来

services.AddAuthorization(options =>
{
    options.AddPolicy("Over18", 
        policy => policy.Requirements.Add(new Authorization.Over18Requirement()));
});

最后,将其应用于控制器或操作方法

[Authorize(Policy = "Over18")]

【讨论】:

  • 我不得不评论一下,这一切都比实现自定义授权方法更复杂。我知道我希望如何完成授权,我可以直接在 MVC 5 中编写它,在 MVC 6 中,他们添加了许多“完成”代码,这些代码实际上比实现核心“事物”本身更难理解。让我坐在一个页面前试图找出一些东西而不是直接编写代码,这对于使用 Microsoft(或 No-Sql)以外的 RDBMS 的人来说也是一个很大的痛苦。
  • 我和这些 cmets 中的许多其他人一样,非常失望的是,使用属性进行授权已经大大削弱了 Web API 2 中的可能性。对不起,伙计们,但你的“要求”抽象未能涵盖我们以前可以使用属性构造函数参数来通知底层授权算法的任何情况。过去,做[CustomAuthorize(Operator.And, Permission.GetUser, Permission.ModifyUser)] 之类的事情非常简单。我可以通过修改构造函数参数以无数种方式使用单个自定义属性。
  • 我也很震惊,自称“领先的 ASP.NET 安全专家”实际上建议使用魔术字符串(破解 IAuthorizeData.Policy 的含义)和自定义策略提供程序来克服这种公然的疏忽,而不是在框架内解决它。我以为我们不应该创建自己的实现?您让我们中的一些人别无选择,只能(再次)从头开始重新实现授权,而这一次甚至没有 Web API 的旧 Authorize 属性的好处。现在我们必须在动作过滤器或中间件级别上执行此操作。
  • “生活真的很简单,但我们坚持让它变得复杂。” - 孔子
【解决方案3】:

看来使用ASP.NET Core 2,你可以再次继承AuthorizeAttribute,你只需要同时实现IAuthorizationFilter(或IAsyncAuthorizationFilter):

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)]
public class CustomAuthorizeAttribute : AuthorizeAttribute, IAuthorizationFilter
{
    private readonly string _someFilterParameter;

    public CustomAuthorizeAttribute(string someFilterParameter)
    {
        _someFilterParameter = someFilterParameter;
    }

    public void OnAuthorization(AuthorizationFilterContext context)
    {
        var user = context.HttpContext.User;

        if (!user.Identity.IsAuthenticated)
        {
            // it isn't needed to set unauthorized result 
            // as the base class already requires the user to be authenticated
            // this also makes redirect to a login page work properly
            // context.Result = new UnauthorizedResult();
            return;
        }

        // you can also use registered services
        var someService = context.HttpContext.RequestServices.GetService<ISomeService>();

        var isAuthorized = someService.IsUserAuthorized(user.Identity.Name, _someFilterParameter);
        if (!isAuthorized)
        {
            context.Result = new StatusCodeResult((int)System.Net.HttpStatusCode.Forbidden);
            return;
        }
    }
}

【讨论】:

  • 所以只能用这个来deny授权,不能grant呢?
  • @MEMark 授予,你的意思是覆盖另一个授权属性?
  • AFAIK,默认情况下允许访问,因此您需要明确拒绝它(例如,通过添加 AuthorizeAttribute)。检查此问题以获取更多详细信息:stackoverflow.com/questions/17272422/…
  • 另请注意,在建议的示例中,不必从 AuthorizeAttribute 继承。您可以从 AttributeIAuthorizationFilter 继承。这样,如果使用一些非标准的身份验证机制,您将不会收到以下异常:InvalidOperationException: No authenticationScheme is specified, and there is no DefaultChallengeScheme found.
  • 请注意,如果您的OnAuthorization 实现需要等待异步方法,您应该实现IAsyncAuthorizationFilter 而不是IAuthorizationFilter 否则您的过滤器将同步执行并且您的控制器操作将执行而不管结果如何过滤器。
【解决方案4】:

根据 Derek Greer GREAT 的回答,我用枚举做到了。

这是我的代码示例:

public enum PermissionItem
{
    User,
    Product,
    Contact,
    Review,
    Client
}

public enum PermissionAction
{
    Read,
    Create,
}


public class AuthorizeAttribute : TypeFilterAttribute
{
    public AuthorizeAttribute(PermissionItem item, PermissionAction action)
    : base(typeof(AuthorizeActionFilter))
    {
        Arguments = new object[] { item, action };
    }
}

public class AuthorizeActionFilter : IAuthorizationFilter
{
    private readonly PermissionItem _item;
    private readonly PermissionAction _action;
    public AuthorizeActionFilter(PermissionItem item, PermissionAction action)
    {
        _item = item;
        _action = action;
    }
    public void OnAuthorization(AuthorizationFilterContext context)
    {
        bool isAuthorized = MumboJumboFunction(context.HttpContext.User, _item, _action); // :)

        if (!isAuthorized)
        {
            context.Result = new ForbidResult();
        }
    }
}

public class UserController : BaseController
{
    private readonly DbContext _context;

    public UserController( DbContext context) :
        base()
    {
        _logger = logger;
    }

    [Authorize(PermissionItem.User, PermissionAction.Read)]
    public async Task<IActionResult> Index()
    {
        return View(await _context.User.ToListAsync());
    }
}

【讨论】:

  • 谢谢。我创建了这篇文章,实现略有不同,并请求验证stackoverflow.com/questions/49551047/…
  • MumboJumboFunction
  • 你没有在你的回答中显示如何将其应用于用户,即你如何存储当前用户的权限位
  • @rogue39nin,您可以使用 Claims (context.HttpContext.User.Claims) 为您的令牌添加一些额外的 public 元数据。您还可以使用数据库、调用外部服务或任何其他允许您获取该信息的方法。
  • @bruno.almeida,这很好用。不过,我将如何在 Razor 视图中使用它?
【解决方案5】:

您可以创建自己的 AuthorizationHandler,它将在您的控制器和操作上找到自定义属性,并将它们传递给 HandleRequirementAsync 方法。

public abstract class AttributeAuthorizationHandler<TRequirement, TAttribute> : AuthorizationHandler<TRequirement> where TRequirement : IAuthorizationRequirement where TAttribute : Attribute
{
    protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, TRequirement requirement)
    {
        var attributes = new List<TAttribute>();

        var action = (context.Resource as AuthorizationFilterContext)?.ActionDescriptor as ControllerActionDescriptor;
        if (action != null)
        {
            attributes.AddRange(GetAttributes(action.ControllerTypeInfo.UnderlyingSystemType));
            attributes.AddRange(GetAttributes(action.MethodInfo));
        }

        return HandleRequirementAsync(context, requirement, attributes);
    }

    protected abstract Task HandleRequirementAsync(AuthorizationHandlerContext context, TRequirement requirement, IEnumerable<TAttribute> attributes);

    private static IEnumerable<TAttribute> GetAttributes(MemberInfo memberInfo)
    {
        return memberInfo.GetCustomAttributes(typeof(TAttribute), false).Cast<TAttribute>();
    }
}

然后,您可以将它用于您的控制器或操作所需的任何自定义属性。例如添加权限要求。只需创建您的自定义属性。

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true)]
public class PermissionAttribute : AuthorizeAttribute
{
    public string Name { get; }

    public PermissionAttribute(string name) : base("Permission")
    {
        Name = name;
    }
}

然后创建一个要求以添加到您的策略中

public class PermissionAuthorizationRequirement : IAuthorizationRequirement
{
    //Add any custom requirement properties if you have them
}

然后为您的自定义属性创建 AuthorizationHandler,继承我们之前创建的 AttributeAuthorizationHandler。它将在 HandleRequirementsAsync 方法中为您的所有自定义属性传递一个 IEnumerable,从您的 Controller 和 Action 累积。

public class PermissionAuthorizationHandler : AttributeAuthorizationHandler<PermissionAuthorizationRequirement, PermissionAttribute>
{
    protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, PermissionAuthorizationRequirement requirement, IEnumerable<PermissionAttribute> attributes)
    {
        foreach (var permissionAttribute in attributes)
        {
            if (!await AuthorizeAsync(context.User, permissionAttribute.Name))
            {
                return;
            }
        }

        context.Succeed(requirement);
    }

    private Task<bool> AuthorizeAsync(ClaimsPrincipal user, string permission)
    {
        //Implement your custom user permission logic here
    }
}

最后,在您的 Startup.cs ConfigureServices 方法中,将您的自定义 AuthorizationHandler 添加到服务中,并添加您的 Policy。

        services.AddSingleton<IAuthorizationHandler, PermissionAuthorizationHandler>();

        services.AddAuthorization(options =>
        {
            options.AddPolicy("Permission", policyBuilder =>
            {
                policyBuilder.Requirements.Add(new PermissionAuthorizationRequirement());
            });
        });

现在您可以使用自定义属性简单地装饰您的控制器和操作。

[Permission("AccessCustomers")]
public class CustomersController
{
    [Permission("AddCustomer")]
    IActionResult AddCustomer([FromBody] Customer customer)
    {
        //Add customer
    }
}

【讨论】:

  • 这是相当过度设计的......我使用一个简单的 AuthorizationFilterAttribute 解决了同样的问题,它接收一个参数。您不需要对此进行反思,它似乎比“官方”解决方案(我觉得很差)更加狡猾。
  • @Vi100 我在 ASP.NET Core 中找不到关于 AuthorizationFilters 的太多信息。官方文档页面说他们目前正在研究这个主题。 docs.microsoft.com/en-us/aspnet/core/security/authorization/…
  • @Vi100 你能否分享你的解决方案,如果有更简单的方法来实现这一点,我很想知道。
  • 我其实很喜欢这个解决方案,它利用了新的策略系统并结合了属性来提供一个非常干净的解决方案。我使用全局 Authorize 属性来确保用户已登录,然后在需要时应用权限策略。
  • 有一点需要注意,上面使用 UnderlyingSystemType 无法编译,但删除它似乎可行。
【解决方案6】:

目前制作自定义 AuthorizeAttribute 的方法是什么

对于纯授权场景(如仅限制特定用户的访问),推荐的方法是使用新的授权块:https://github.com/aspnet/MusicStore/blob/1c0aeb08bb1ebd846726232226279bbe001782e1/samples/MusicStore/Startup.cs#L84-L92

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.Configure<AuthorizationOptions>(options =>
        {
            options.AddPolicy("ManageStore", policy => policy.RequireClaim("Action", "ManageStore"));
        });
    }
}

public class StoreController : Controller
{
    [Authorize(Policy = "ManageStore"), HttpGet]
    public async Task<IActionResult> Manage() { ... }
}

对于身份验证,最好在中间件级别处理。

你到底想达到什么目的?

【讨论】:

  • 我在标头授权中收到一个会话 ID。通过该 ID,我将知道特定操作是否有效。
  • 那么这不是授权问题。我猜你的“会话 ID”实际上是一个包含调用者身份的令牌:这绝对应该在中间件级别完成。
  • 这不是身份验证(确定用户是谁),而是授权(确定用户是否应该有权访问资源)。那么你建议我在哪里解决这个问题?
  • @jltrem,同意,您说的是授权,而不是身份验证。
  • @Pinpoint 我不是。我向另一个系统查询该信息。该系统进行身份验证(确定用户)并授权(告诉我该用户可以访问什么)。现在,我通过在每个控制器操作中调用一个方法让其他系统验证会话来让它工作。我希望通过属性自动发生这种情况。
【解决方案7】:

现代方式是 AuthenticationHandlers

在startup.cs中添加

services.AddAuthentication("BasicAuthentication").AddScheme<AuthenticationSchemeOptions, BasicAuthenticationHandler>("BasicAuthentication", null);

public class BasicAuthenticationHandler : AuthenticationHandler<AuthenticationSchemeOptions>
    {
        private readonly IUserService _userService;

        public BasicAuthenticationHandler(
            IOptionsMonitor<AuthenticationSchemeOptions> options,
            ILoggerFactory logger,
            UrlEncoder encoder,
            ISystemClock clock,
            IUserService userService)
            : base(options, logger, encoder, clock)
        {
            _userService = userService;
        }

        protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
        {
            if (!Request.Headers.ContainsKey("Authorization"))
                return AuthenticateResult.Fail("Missing Authorization Header");

            User user = null;
            try
            {
                var authHeader = AuthenticationHeaderValue.Parse(Request.Headers["Authorization"]);
                var credentialBytes = Convert.FromBase64String(authHeader.Parameter);
                var credentials = Encoding.UTF8.GetString(credentialBytes).Split(new[] { ':' }, 2);
                var username = credentials[0];
                var password = credentials[1];
                user = await _userService.Authenticate(username, password);
            }
            catch
            {
                return AuthenticateResult.Fail("Invalid Authorization Header");
            }

            if (user == null)
                return AuthenticateResult.Fail("Invalid User-name or Password");

            var claims = new[] {
                new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()),
                new Claim(ClaimTypes.Name, user.Username),
            };
            var identity = new ClaimsIdentity(claims, Scheme.Name);
            var principal = new ClaimsPrincipal(identity);
            var ticket = new AuthenticationTicket(principal, Scheme.Name);

            return AuthenticateResult.Success(ticket);
        }
    }

IUserService 是您在拥有用户名和密码的情况下创建的服务。 基本上它会返回一个用户类,您可以使用它来映射您的声明。

var claims = new[] {
                new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()),
                new Claim(ClaimTypes.Name, user.Username),
            }; 

然后你可以查询这些声明和你映射的任何数据,还有很多,看看 ClaimTypes 类

您可以在扩展方法中使用它来获取任何映射

public int? GetUserId()
{
   if (context.User.Identity.IsAuthenticated)
    {
       var id=context.User.FindFirst(ClaimTypes.NameIdentifier);
       if (!(id is null) && int.TryParse(id.Value, out var userId))
            return userId;
     }
      return new Nullable<int>();
 }

我认为这种新方式比这里显示的旧方式更好,两者都有效

public class BasicAuthenticationAttribute : AuthorizationFilterAttribute
{
    public override void OnAuthorization(HttpActionContext actionContext)
    {
        if (actionContext.Request.Headers.Authorization != null)
        {
            var authToken = actionContext.Request.Headers.Authorization.Parameter;
            // decoding authToken we get decode value in 'Username:Password' format
            var decodeauthToken = System.Text.Encoding.UTF8.GetString(Convert.FromBase64String(authToken));
            // spliting decodeauthToken using ':'
            var arrUserNameandPassword = decodeauthToken.Split(':');
            // at 0th postion of array we get username and at 1st we get password
            if (IsAuthorizedUser(arrUserNameandPassword[0], arrUserNameandPassword[1]))
            {
                // setting current principle
                Thread.CurrentPrincipal = new GenericPrincipal(new GenericIdentity(arrUserNameandPassword[0]), null);
            }
            else
            {
                actionContext.Response = actionContext.Request.CreateResponse(HttpStatusCode.Unauthorized);
            }
        }
        else
        {
            actionContext.Response = actionContext.Request.CreateResponse(HttpStatusCode.Unauthorized);
        }
    }

    public static bool IsAuthorizedUser(string Username, string Password)
    {
        // In this method we can handle our database logic here...
        return Username.Equals("test") && Password == "test";
    }
}

【讨论】:

  • 这个绝妙的答案就像一个魅力!谢谢你,我希望你能得到支持,因为这是我在搜索博客、文档和堆栈以进行基本身份验证和角色授权六个小时后找到的最佳答案。
  • @PiotrŚródka,不客气,请注意答案有点“简化”,请测试文本中是否有“:”,因为恶意用户可能会尝试通过简单地使您的服务崩溃在索引超出范围异常中没有发挥好结尾。一如既往地测试外部资源提供给您的内容
  • 这真的很有帮助。我需要做的另一件事是确保 app.UseAuthentication();之前是 app.UseAuthorization();
  • 这是正确的解决方案。有时人们会混淆授权和身份验证。这是处理身份验证的方法。
  • 如果我想调用外部身份验证并获得一个有过期时间的令牌怎么办?我将如何处理授权?在您的情况下,您调用数据库以检索用户,如果未检索到,则该用户未经授权。我想使用令牌执行此操作,但我不想将其保存在数据库中。
【解决方案8】:

什么?!

我决定添加另一个简单的答案。 B/c 我发现这些答案中的大多数都有些过度设计。也因为我需要一种方式来授予授权,而不仅仅是拒绝它。这里的大多数答案都提供了一种“加强”安全性的方法,但我想“放松”它。例如:“如果配置了某些应用程序设置,则允许匿名用户访问”。

public class MyAuthAttribute : Attribute, IAuthorizationFilter
{
    public void OnAuthorization(AuthorizationFilterContext context)
    {
        //check access 
        if (CheckPermissions())
        {
            //all good, add some code if you want. Or don't
        }
        else
        {
            //DENIED!
            //return "ChallengeResult" to redirect to login page (for example)
            context.Result = new ChallengeResult(CookieAuthenticationDefaults.AuthenticationScheme);
        }
    }
}

就是这样。不用纠结“policies”、“claims”、“handlers”等[哔]

用法:

// GET api/Get/5
[MyAuth]
public ActionResult<string> Get(int id)
{
    return "blahblah";
}

【讨论】:

  • 谢谢,终于有一个简单的解决方案了!在所有过度设计的混乱中很难找到。
【解决方案9】:

如果有人只想在授权阶段使用当前的安全实践验证不记名令牌,

将此添加到您的启动/配置服务中

    services.AddSingleton<IAuthorizationHandler, BearerAuthorizationHandler>();
    services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer();

    services.AddAuthorization(options => options.AddPolicy("Bearer",
        policy => policy.AddRequirements(new BearerRequirement())
        )
    );

这在你的代码库中,

public class BearerRequirement : IAuthorizationRequirement
{
    public async Task<bool> IsTokenValid(SomeValidationContext context, string token)
    {
        // here you can check if the token received is valid 
        return true;
    }
}

public class BearerAuthorizationHandler : AuthorizationHandler<BearerRequirement> 
{

    public BearerAuthorizationHandler(SomeValidationContext thatYouCanInject)
    {
       ...
    }

    protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, BearerRequirement requirement)
    {
        var authFilterCtx = (Microsoft.AspNetCore.Mvc.Filters.AuthorizationFilterContext)context.Resource;
        string authHeader = authFilterCtx.HttpContext.Request.Headers["Authorization"];
        if (authHeader != null && authHeader.Contains("Bearer"))
        {
            var token = authHeader.Replace("Bearer ", string.Empty);
            if (await requirement.IsTokenValid(thatYouCanInject, token))
            {
                context.Succeed(requirement);
            }
        }
    }
}

如果代码没有到达context.Succeed(...),它无论如何都会失败 (401)。

然后你可以在你的控制器中使用

 [Authorize(Policy = "Bearer", AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]

【讨论】:

  • 当 JwtBearer 中间件已经解决了这个问题时,为什么还要选择执行自己的令牌验证?它还将正确的内容放在 WWW-Authenticate 响应标头中,以应对身份验证/令牌验证/过期失败。如果您想访问身份验证管道,您可以利用 AddJwtBearer 选项(OnAuthenticationFailed、OnChallenge、OnMessageReceived 和 OnTokenValidated)上的特定事件。
  • 这比我见过的任何其他解决方案都简单得多。特别是对于简单的 api 密钥用例。一个更新:对于 3.1,由于端点路由的东西,对 AuthorizationFilterContext 的强制转换不再有效。您需要通过 HttpContextAccessor 获取上下文。
【解决方案10】:

接受的答案 (https://stackoverflow.com/a/41348219/4974715) 实际上是不可维护或不适合的,因为“CanReadResource”被用作声明(但实际上应该是实际的政策,IMO)。答案的方法在使用方式上并不好,因为如果一个操作方法需要许多不同的声明设置,那么使用该答案,您将不得不重复编写类似...

[ClaimRequirement(MyClaimTypes.Permission, "CanReadResource")] 
[ClaimRequirement(MyClaimTypes.AnotherPermision, "AnotherClaimVaue")]
//and etc. on a single action.

所以,想象一下这需要多少编码。理想情况下,“CanReadResource”应该是一个使用许多声明来确定用户是否可以读取资源的策略。

我所做的是将我的策略创建为一个枚举,然后循环并设置这样的要求......

services.AddAuthorization(authorizationOptions =>
        {
            foreach (var policyString in Enum.GetNames(typeof(Enumerations.Security.Policy)))
            {
                authorizationOptions.AddPolicy(
                    policyString,
                    authorizationPolicyBuilder => authorizationPolicyBuilder.Requirements.Add(new DefaultAuthorizationRequirement((Enumerations.Security.Policy)Enum.Parse(typeof(Enumerations.Security.Policy), policyWrtString), DateTime.UtcNow)));

      /* Note that thisn does not stop you from 
          configuring policies directly against a username, claims, roles, etc. You can do the usual.
     */
            }
        }); 

DefaultAuthorizationRequirement 类看起来像...

public class DefaultAuthorizationRequirement : IAuthorizationRequirement
{
    public Enumerations.Security.Policy Policy {get; set;} //This is a mere enumeration whose code is not shown.
    public DateTime DateTimeOfSetup {get; set;} //Just in case you have to know when the app started up. And you may want to log out a user if their profile was modified after this date-time, etc.
}

public class DefaultAuthorizationHandler : AuthorizationHandler<DefaultAuthorizationRequirement>
{
    private IAServiceToUse _aServiceToUse;

    public DefaultAuthorizationHandler(
        IAServiceToUse aServiceToUse
        )
    {
        _aServiceToUse = aServiceToUse;
    }

    protected async override Task HandleRequirementAsync(AuthorizationHandlerContext context, DefaultAuthorizationRequirement requirement)
    {
        /*Here, you can quickly check a data source or Web API or etc. 
           to know the latest date-time of the user's profile modification...
        */
        if (_aServiceToUse.GetDateTimeOfLatestUserProfileModication > requirement.DateTimeOfSetup)
        {
            context.Fail(); /*Because any modifications to user information, 
            e.g. if the user used another browser or if by Admin modification, 
            the claims of the user in this session cannot be guaranteed to be reliable.
            */
            return;
        }

        bool shouldSucceed = false; //This should first be false, because context.Succeed(...) has to only be called if the requirement specifically succeeds.

        bool shouldFail = false; /*This should first be false, because context.Fail() 
        doesn't have to be called if there's no security breach.
        */

        // You can do anything.
        await doAnythingAsync();

       /*You can get the user's claims... 
          ALSO, note that if you have a way to priorly map users or users with certain claims 
          to particular policies, add those policies as claims of the user for the sake of ease. 
          BUT policies that require dynamic code (e.g. checking for age range) would have to be 
          coded in the switch-case below to determine stuff.
       */

        var claims = context.User.Claims;

        // You can, of course, get the policy that was hit...
        var policy = requirement.Policy

        //You can use a switch case to determine what policy to deal with here...
        switch (policy)
        {
            case Enumerations.Security.Policy.CanReadResource:
                 /*Do stuff with the claims and change the 
                     value of shouldSucceed and/or shouldFail.
                */
                 break;
            case Enumerations.Security.Policy.AnotherPolicy:
                 /*Do stuff with the claims and change the 
                    value of shouldSucceed and/or shouldFail.
                 */
                 break;
                // Other policies too.

            default:
                 throw new NotImplementedException();
        }

        /* Note that the following conditions are 
            so because failure and success in a requirement handler 
            are not mutually exclusive. They demand certainty.
        */

        if (shouldFail)
        {
            context.Fail(); /*Check the docs on this method to 
            see its implications.
            */
        }                

        if (shouldSucceed)
        {
            context.Succeed(requirement); 
        } 
     }
}

请注意,上面的代码还可以将用户预映射到数据存储中的策略。因此,在为用户撰写声明时,您基本上会直接或间接检索已预先映射到用户的策略(例如,因为用户具有特定声明值并且该声明值已被识别并映射到策略,例如它为也具有该声明值的用户提供自动映射),并将策略作为声明登记,这样在授权处理程序中,您可以简单地检查用户的声明是否包含 require.Policy 作为其声明项的值索赔。那是为了满足策略要求的静态方式,例如“名字”要求本质上是静态的。因此,对于上面的示例(我在之前对此答案的更新中忘记提供有关 Authorize 属性的示例),使用具有 Authorize 属性的策略如下所示,其中 ViewRecord 是枚举成员:

[Authorize(Policy = nameof(Enumerations.Security.Policy.ViewRecord))] 

动态要求可以是关于检查年龄范围等,并且使用这些要求的政策不能预先映射到用户。

@blowdart (https://stackoverflow.com/a/31465227/4974715) 已经给出了动态政策声明检查的示例(例如检查用户是否超过 18 岁)。

PS:这是我在手机上输入的。请原谅任何拼写错误和格式缺失。

【讨论】:

  • 恕我直言,Policy 更像是一个带有自定义逻辑的静态验证过程,目前它不能像旧的AuthorizeAttribute 那样简单地参数化。您必须在应用程序启动期间生成所有可能的DefaultAuthorizationRequirement 实例才能在控制器中使用它们。我希望有一个可以接受一些标量参数(可能是无限组合)的策略。这样我就不会破坏开闭原则。你的例子确实如此。 (无论如何我很感激)
  • @neleus,您必须使用接受资源的要求。例如,在原始问题中,该资源是 SessionID。在您的评论中,资源是您正在谈论的标量属性。因此,在需求内部,将根据用户的声明对资源进行评估,然后确定授权是成功还是失败。
  • @neleus,已经,用户应该已经过身份验证并且还被授权调用控制器操作,但是我刚刚描述的要求将在控制器操作中使用,以确定用户是否可以更进一步基于提供给它的资源中包含的信息。资源可以来自请求头、查询字符串、从数据库中获取的数据等。如果您对此感兴趣,我可以编写代码。
  • 你的意思是具体的授权决定是控制者的工作而不是要求?
  • 我真的不明白这能解决什么问题。我个人会避免在这里传递两件事,只使用参数来传递,但是需要许多权限枚举。如果您需要传入大量权限,那么我可以看到通过这些静态枚举创建策略是可以的。这并不难,要么你需要策略,要么你不需要。没有“正确”的方式。
【解决方案11】:

以下代码在 .Net Core 5 中对我有用

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class AccessAuthorizationAttribute : AuthorizeAttribute, IAuthorizationFilter
{
    public string Module { get; set; } //Permission string to get from controller

    public AccessAuthorizationAttribute(string module)
    {
        Module = module;
    }
    public void OnAuthorization(AuthorizationFilterContext context)
    {
        //Validate if any permissions are passed when using attribute at controller or action level

        if (string.IsNullOrEmpty(Module))
        {
            //Validation cannot take place without any permissions so returning unauthorized
            context.Result = new UnauthorizedResult();
            return;
        }
       
        if (hasAccess)
        {
            return;
        }

        context.Result = new UnauthorizedResult();
        return;
    }
}

【讨论】:

  • 授权失败时,要返回403,而不是401。
【解决方案12】:

在撰写本文时,我相信这可以通过 asp.net core 2 及更高版本中的 IClaimsTransformation 接口来完成。我刚刚实现了一个概念验证,可以在此处发布。

public class PrivilegesToClaimsTransformer : IClaimsTransformation
{
    private readonly IPrivilegeProvider privilegeProvider;
    public const string DidItClaim = "http://foo.bar/privileges/resolved";

    public PrivilegesToClaimsTransformer(IPrivilegeProvider privilegeProvider)
    {
        this.privilegeProvider = privilegeProvider;
    }

    public async Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal)
    {
        if (principal.Identity is ClaimsIdentity claimer)
        {
            if (claimer.HasClaim(DidItClaim, bool.TrueString))
            {
                return principal;
            }

            var privileges = await this.privilegeProvider.GetPrivileges( ... );
            claimer.AddClaim(new Claim(DidItClaim, bool.TrueString));

            foreach (var privilegeAsRole in privileges)
            {
                claimer.AddClaim(new Claim(ClaimTypes.Role /*"http://schemas.microsoft.com/ws/2008/06/identity/claims/role" */, privilegeAsRole));
            }
        }

        return principal;
    }
}

要在你的控制器中使用它,只需在你的方法中添加一个适当的[Authorize(Roles="whatever")]

[HttpGet]
[Route("poc")]
[Authorize(Roles = "plugh,blast")]
public JsonResult PocAuthorization()
{
    var result = Json(new
    {
        when = DateTime.UtcNow,
    });

    result.StatusCode = (int)HttpStatusCode.OK;

    return result;
}

在我们的例子中,每个请求都包含一个作为 JWT 的 Authorization 标头。这是原型,我相信下周我们会在我们的生产系统中做一些与此非常接近的事情。

未来的选民,投票时请考虑写作日期。到今天为止,这个works on my machine.™ 您可能需要更多的错误处理和日志记录您的实现。

【讨论】:

  • ConfigureServices 怎么样?需要加点什么吗?
  • 正如其他地方所讨论的,是的。
【解决方案13】:

只是添加了@Shawn 的精彩回答。如果您使用的是 dotnet 5,则需要将类更新为:

public abstract class AttributeAuthorizationHandler<TRequirement, TAttribute> : AuthorizationHandler<TRequirement> where TRequirement : IAuthorizationRequirement where TAttribute : Attribute
{
    protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, TRequirement requirement)
    {
        var attributes = new List<TAttribute>();
        
        if (context.Resource is HttpContext httpContext)
        {
            var endPoint = httpContext.GetEndpoint();

            var action = endPoint?.Metadata.GetMetadata<ControllerActionDescriptor>();

            if(action != null)
            {
                attributes.AddRange(GetAttributes(action.ControllerTypeInfo.UnderlyingSystemType));
                attributes.AddRange(GetAttributes(action.MethodInfo));
            }
        }
        
        return HandleRequirementAsync(context, requirement, attributes);
    }

    protected abstract Task HandleRequirementAsync(AuthorizationHandlerContext context, TRequirement requirement, IEnumerable<TAttribute> attributes);

    private static IEnumerable<TAttribute> GetAttributes(MemberInfo memberInfo) => memberInfo.GetCustomAttributes(typeof(TAttribute), false).Cast<TAttribute>();
}

注意获取 ControllerActionDescriptor 的方式发生了变化。

【讨论】:

    【解决方案14】:

    我有不记名令牌,我可以阅读索赔。 我在控制器和操作上使用该属性

    public class CustomAuthorizationAttribute : ActionFilterAttribute
    {
        public string[] Claims;
    
        public override void OnActionExecuting(ActionExecutingContext context)
        {
            // check user 
            var contextUser = context?.HttpContext?.User;
            if (contextUser == null)
            {
                throw new BusinessException("Forbidden");
            }
    
    
            // check roles
            var roles = contextUser.FindAll("http://schemas.microsoft.com/ws/2008/06/identity/claims/role").Select(c => c.Value).ToList();
            if (!roles.Any(s => Claims.Contains(s)))
            {
                throw new BusinessException("Forbidden");
            }
    
            base.OnActionExecuting(context);
        }
    }
    

    例子

    [CustomAuthorization(Claims = new string[]
        {
            nameof(AuthorizationRole.HR_ADMIN),
            nameof(AuthorizationRole.HR_SETTING)
        })]
    [Route("api/[controller]")]
    [ApiController]
    public class SomeAdminController : ControllerBase
    {
        private readonly IMediator _mediator;
    
        public SomeAdminController(IMediator mediator)
        {
            _mediator = mediator;
        }
    
        [HttpGet("list/SomeList")]
        public async Task<IActionResult> SomeList()
            => Ok(await _mediator.Send(new SomeListQuery()));
    }
    

    那是角色

    public struct AuthorizationRole
    {
        public static string HR_ADMIN;
        public static string HR_SETTING;
    }
    

    【讨论】:

      【解决方案15】:

      在我们的应用程序中进行授权。我们必须根据授权属性中传递的参数调用服务。

      例如,如果我们想检查登录的医生是否可以查看患者预约,我们会将“View_Appointment”传递给自定义授权属性,并在数据库服务中检查该权限,并根据结果进行授权。下面是这个场景的代码:

          public class PatientAuthorizeAttribute : TypeFilterAttribute
          {
          public PatientAuthorizeAttribute(params PatientAccessRights[] right) : base(typeof(AuthFilter)) //PatientAccessRights is an enum
          {
              Arguments = new object[] { right };
          }
      
          private class AuthFilter : IActionFilter
          {
              PatientAccessRights[] right;
      
              IAuthService authService;
      
              public AuthFilter(IAuthService authService, PatientAccessRights[] right)
              {
                  this.right = right;
                  this.authService = authService;
              }
      
              public void OnActionExecuted(ActionExecutedContext context)
              {
              }
      
              public void OnActionExecuting(ActionExecutingContext context)
              {
                  var allparameters = context.ActionArguments.Values;
                  if (allparameters.Count() == 1)
                  {
                      var param = allparameters.First();
                      if (typeof(IPatientRequest).IsAssignableFrom(param.GetType()))
                      {
                          IPatientRequest patientRequestInfo = (IPatientRequest)param;
                          PatientAccessRequest userAccessRequest = new PatientAccessRequest();
                          userAccessRequest.Rights = right;
                          userAccessRequest.MemberID = patientRequestInfo.PatientID;
                          var result = authService.CheckUserPatientAccess(userAccessRequest).Result; //this calls DB service to check from DB
                          if (result.Status == ReturnType.Failure)
                          {
                              //TODO: return apirepsonse
                              context.Result = new StatusCodeResult((int)System.Net.HttpStatusCode.Forbidden);
                          }
                      }
                      else
                      {
                          throw new AppSystemException("PatientAuthorizeAttribute not supported");
                      }
                  }
                  else
                  {
                      throw new AppSystemException("PatientAuthorizeAttribute not supported");
                  }
              }
          }
      }
      

      在 API 操作中,我们像这样使用它:

          [PatientAuthorize(PatientAccessRights.PATIENT_VIEW_APPOINTMENTS)] //this is enum, we can pass multiple
          [HttpPost]
          public SomeReturnType ViewAppointments()
          {
      
          }
      

      【讨论】:

      • 请注意,当您想对 SignalR 中的 Hub 方法使用相同的属性时,IActionFilter 将成为问题。SignalR Hub 需要 IAuthorizationFilter
      • 感谢您的信息。我现在没有在我的应用程序中使用 SignalR,所以我没有用它测试过。
      • 原理相同,我猜你仍然必须使用标头的授权条目,实现会有所不同
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2016-10-07
      • 1970-01-01
      • 2017-06-21
      相关资源
      最近更新 更多