在ASP.NET Core中如何创建自定义的AuthorizeAttribute?

649
我试图在ASP.NET Core中创建自定义的授权属性。 在之前的版本中,可以重写bool AuthorizeCore(HttpContextBase httpContext)。 但在AuthorizeAttribute中不再存在此功能。
当前制定自定义AuthorizeAttribute的方法是什么?
我想要实现的目标是:从Header Authorization中获取会话ID,然后根据该ID确定特定操作是否有效。

1
我不确定如何做,但MVC是开源的。你可以拉取GitHub仓库并寻找IAuthorizationFilter的实现。如果我今天有时间,我会帮你找到并发布一个真正的答案,但不能保证。GitHub仓库链接:https://github.com/aspnet/Mvc - bopapa_1979
1
好的,时间有点紧,但请在MVC Repo中寻找AuthorizationPolicy的用法,该Repo使用AuthorizeAttribute,在aspnet/Security Repo中可以找到,链接为:https://github.com/aspnet/Security。或者,在MVC Repo中查找您关心的安全内容所在的命名空间,即Microsoft.AspNet.Authorization。很抱歉我无法提供更多帮助,祝你好运! - bopapa_1979
18个回答

7

被认可的答案(https://dev59.com/CVwZ5IYBdhLWcg3wbv7r#41348219)在实际维护和适用方面并不现实,因为 "CanReadResource" 被用作声明(但实际上在我看来应该基本上是一项政策)。 答案中的方法在使用方式上并不正确,因为如果一个操作方法需要许多不同的声明设置,则使用该答案后,您将不得不反复编写以下内容...

[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); 
        } 
     }
}

请注意,以上代码还可以启用将用户预映射到数据存储中的策略。因此,在为用户组合声明时,您基本上会直接或间接地检索已预先映射到用户的策略(例如,因为用户具有某些声明值,并且将该声明值标识和映射到策略,从而为具有该声明值的用户提供自动映射),并将这些策略作为声明列出,以便在授权处理程序中,您可以简单地检查用户的声明是否包含要求中的策略作为声明项的值。这适用于静态满足策略要求的方式,例如,“名字”要求在本质上是静态的。因此,对于上面的示例(我在早期更新答案时忘记给出Authorize属性的示例),使用带有Authorize属性的策略如下所示,其中ViewRecord是枚举成员:
[Authorize(Policy = nameof(Enumerations.Security.Policy.ViewRecord))] 

动态需求可以检查年龄范围等,使用此类要求的策略无法预先映射到用户。

动态策略声明检查的一个例子(例如,检查用户是否年满18岁)已经在@blowdart给出的答案中提供了(https://dev59.com/CVwZ5IYBdhLWcg3wbv7r#31465227)。

PS:我是用手机输入的。请原谅任何拼写错误和格式不整齐。


在我看来,策略更像是一个带有自定义逻辑的静态验证过程,目前它不能像旧的AuthorizeAttribute那样轻松地进行参数化。您必须在应用程序启动期间生成所有可能的DefaultAuthorizationRequirement实例才能在控制器中使用它们。我更喜欢有一个可以接受一些标量参数(潜在的无限组合)的策略。这样我就不会违反开放封闭原则。而你的例子却违反了它。(不管怎样,我很感激) - neleus
@neleus,您需要使用一个接受资源的要求。例如,在原始问题中,该资源是SessionID。在您的评论中,该资源是您所讨论的标量属性。因此,在要求内,将根据用户的声明评估该资源,然后确定授权是否应成功或失败。 - Olumide
@neleus,用户应该已经完成了身份验证和授权以调用控制器操作,但我刚才描述的要求将在控制器操作中使用,以根据提供给它的资源中包含的信息确定用户是否可以进一步进行。资源可以来自请求头、查询字符串、从数据库中获取的数据等。如果你有兴趣,我可以编写代码。 - Olumide
你的意思是特定的授权决策更应该由控制器而非需求来完成吗? - neleus
@neleus 不完全是这样,你只是从控制器操作中调用代码。但另一件事情是,你可以将IHttpContextAccessor注册为服务(通过services.AddHttpContextAccessor()),然后将其注入到要求的AuthorizationHandler中。从IHttpContextAccessor中,你可以获取HttpContext.GetRouteData(),然后检索你想要的路由数据(例如讨论中的Seesion ID)。注意空值。 - Olumide
1
我真的不知道这解决了什么问题。我个人会避免在此处传递两个参数,而只是使用params来传递所需的任意数量的权限枚举。如果需要传递大量权限,则可以通过这些静态枚举创建策略。这并不难,你要么需要策略,要么不需要。没有“正确”的方法。 - perustaja

6
截至目前,我相信在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头。这是原型,我相信我们将在下周的生产系统中执行非常接近此设置。
未来的投票者,请考虑写作日期进行投票。截至今天,该在我的机器上有效™。您可能需要在实施中进行更多错误处理和记录。

ConfigureServices怎么样?需要添加什么吗? - Daniel
如在其他地方所讨论的,是的。 - No Refunds No Returns

5
这是一个简单的5步指南,教你如何使用策略实现自定义角色授权,适用于所有复制和粘贴的人 :)。我使用了这些文档
创建一个要求:
public class RoleRequirement : IAuthorizationRequirement
{
    public string Role { get; set; }
}

创建一个处理程序:
public class RoleHandler : AuthorizationHandler<RoleRequirement>
{
    protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, RoleRequirement requirement)
    {
        var requiredRole = requirement.Role;

        //custom auth logic
        //  you can use context to access authenticated user,
        //  you can use dependecy injection to call custom services 

        var hasRole = true;

        if (hasRole)
        {
            context.Succeed(requirement);
        }
        else
        {
            context.Fail(new AuthorizationFailureReason(this, $"Role {requirement.Role} missing"));
        }
    }
}

在 Program.cs 中添加处理程序:
builder.Services.AddSingleton<IAuthorizationHandler, RoleHandler>();

在 program.cs 中添加一个包含您的角色要求的策略:
builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("Read", policy => policy.Requirements.Add(new RoleRequirement{Role = "ReadAccess_Custom_System"}));
});

使用您的政策:
[Authorize("Read")]
public class ExampleController : ControllerBase
{
}

如果有人感兴趣,要求处理程序可以在任何服务生命周期中注册,不一定是单例。https://learn.microsoft.com/en-us/aspnet/core/security/authorization/dependencyinjection?view=aspnetcore-7.0 - Niksr
此外,如果请求未经授权,则结果将返回异常“No authenticationScheme was specified, and there was no DefaultChallengeScheme found”,而不是一些正确的结果代码。因此,需要进行第6步:AuthorizationMiddlewareResultHandler。这里有一个很好的例子https://dev59.com/O1sV5IYBdhLWcg3w4iT-来自@Ogglas - Niksr

2

补充一下@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的方式已经发生了变化。


1
我有Bearer令牌并且可以读取声明。我在控制器和操作上使用该属性。
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;
}

0
很多人已经说过了,但是使用策略处理程序,您可以在.NET Framework的旧方式中实现的东西真的很远。
我按照SO上这个答案的快速撰写进行了跟随:https://dev59.com/gbvoa4cB1Zd3GeqPyCfZ#61963465。对我来说,在制作一些类之后,它完美地运行了。
EditUserRequirement:
public class EditUserRequirement : IAuthorizationRequirement
{
    public EditUserRequirement()
    {
    }
}

一个抽象处理程序,让我的生活更轻松:
public abstract class AbstractRequirementHandler<T> : IAuthorizationHandler
    where T : IAuthorizationRequirement
{
    public async Task HandleAsync(AuthorizationHandlerContext context)
    {
        var pendingRequirements = context.PendingRequirements.ToList();
        foreach (var requirement in pendingRequirements)
        {
            if (requirement is T typedRequirement)
            {
                await HandleRequirementAsync(context, typedRequirement);
            }
        }
    }

    protected abstract Task HandleRequirementAsync(AuthorizationHandlerContext context, T requirement);
}

抽象处理程序的实现:

public class EditUserRequirementHandler : AbstractRequirementHandler<EditUserRequirement>
{
    protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, EditUserRequirement requirement)
    {
        // If the user is owner of the resource, allow it.
        if (IsOwner(context.User, g))
        {
            context.Succeed(requirement);
        }

        return Task.CompletedTask;
    }

    private static bool IsOwner(ClaimsPrincipal user, Guid userIdentifier)
    {
        return user.GetUserIdentifier() == userIdentifier;
    }
}

注册我的处理程序和要求: services.AddSingleton<IAuthorizationHandler, EditUserRequirementHandler>();

        services.AddAuthorization(options =>
        {
            options.AddPolicy(Policies.Policies.EditUser, policy =>
            {
                policy.Requirements.Add(new EditUserRequirement());
            });
        });

然后在 Blazor 中使用我的策略:

<AuthorizeView Policy="@Policies.EditUser" Resource="@id">
    <NotAuthorized>
        <Unauthorized />
    </NotAuthorized>
    <Authorized Context="Auth">
        ...
    </Authorized>
</AuthorizeView>

我希望这对于任何遇到这个问题的人都有用。


0

0

为了在我们的应用程序中进行授权,我们需要根据授权属性中传递的参数调用一个服务。

例如,如果我们想要检查已登录的医生是否可以查看患者的预约,我们将在自定义授权属性中传递"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()
    {

    }

4
请注意,当您想在SignalR的Hub方法中使用相同的属性时,IActionFilter可能会成为一个问题。SignalR Hubs需要IAuthorizationFilter。 - ilkerkaran
感谢您提供的信息。我目前的应用程序中没有使用SignalR,因此我还没有与之进行过测试。 - Abdullah
我猜应该是相同的原则,因为您仍然需要使用头部的授权条目,但实现方式可能会有所不同。 - Walter Verhoeven

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接