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


428

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

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

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


我不确定该怎么做,但是MVC是开源的。您可以拉github存储库并查找IAuthorizationFilter的实现。如果我今天有时间,我会寻找您并发布实际答案,但没有任何承诺。github回购: github.com/aspnet/Mvc
bopapa_1979

好的,不合时宜,但是请在以下aspnet / Security存储库中的MVC Repo(使用AuthorizeAttribute)中查找AuthorizationPolicy的用法: github.com/aspnet/Security。或者,在MVC存储库中查找您关心的安全性材料似乎驻留的名称空间,即Microsoft.AspNet.Authorization。抱歉,我帮不上忙。祝好运!
bopapa_1979

Answers:


446

ASP.Net Core团队推荐的方法是使用新的策略设计,在此将其详细记录。新方法的基本思想是使用新的[Authorize]属性来指定“策略”(例如[Authorize( Policy = "YouNeedToBe18ToDoThis")],该策略在应用程序的Startup.cs中注册的位置,以执行某些代码块(即,确保用户拥有年龄限制)年满18岁)。

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

尽管ASP.Net Core Security团队建议不要创建自己的解决方案,但在某些情况下,这可能是最明智的选择。

以下是一个实现,该实现使用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();
    }
}

78
这应该标记为正确答案。在这里,您可以看到Microsoft员工如何看待开发人员的反馈。我不理解为什么他们如此“封闭”,因为拥有许多不同的权限是非常普遍的情况,必须为每个策略编写一个策略是完全过头的。我一直在寻找这个问题……(大约两年前,当vNext仍然在这里打赌时,我已经问过这个问题:stackoverflow.com/questions/32181400/…但我们仍然停留在那儿)
Vi100

3
这是好东西。我们在Web API上提供了身份验证中间件,但在授权权限方面按角色提供了严格的安全性。所以只需要抛出一个属性就可以了:[MyAuthorize(MyClaimTypes.Permission,MyClaimValueTypes.Write,MyPermission.Employee)]看起来很好。
马里亚诺·皮纳多

4
@Derek Greer:这是最好的答案。但是,您要实现一个ActionFilter,该动作将在Authorize Action Filter之后运行。无论如何,有没有实施和授权动作过滤器?
雅各布·潘

6
@JacobPhan您是正确的,使用IAuthorizationFilter接口可以更好地实现这一点。我已经更新了代码以反映更改。
Derek Greer

3
因此new ForbidResult()不起作用(导致异常/ 500),因为它没有关联的授权方案。在这种情况下我将使用什么?
Sinaesthetic,

252

我是asp.net安全人员。首先,我很抱歉,在音乐商店样本或单元测试之外,没有任何文档被记录下来,并且仍然在公开的API方面对其进行了完善。详细的文档在这里

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

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

您在注释中说,您想检查标题中的会话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 dateOfBirth = Convert.ToDateTime(context.User.FindFirst(c => c.Type == ClaimTypes.DateOfBirth).Value);
            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")]

84
我想知道……如何实现这种细粒度的访问控制?假设“ ManageStore音乐商店中的需求”示例。正如示例中那样,只有一种“全部允许或全部不允许”的方式来执行此操作。然后,我们是否必须为每个可能的排列创建新的策略?例如“用户/读取”,“用户/创建”,“用户/ AssignRole”,“用户/删除”,是否需要细粒度的声明?听起来好像要进行大量设置工作才能使其正常运行,并且有大量策略仅用于管理声明而不是[ClaimsAutzorization("User", "Read", "Create", "Delete", "Assign")]属性?

83
我必须指出,所有这些都比实现自定义授权方法更为复杂。我知道我要如何完成授权,我可以直接在MVC 5中编写它,在MVC 6中,他们添加了许多“完成”代码,这些代码实际上比实现核心“事物”本身更难理解。让我坐在页面的前面,试图找出一些问题,而不是直接编写代码,这对于使用RDBMS而非Microsoft(或No-Sql)的人来说也是一个很大的痛苦。
Felype

17
从我的角度来看,这并不能解决所有情况。在MVC 6之前,我使用了自定义的Authorize Attribute来实现自己的“权限系统”。我可以将Authorize属性添加到所有操作,并传递一个特定的所需权限(如Enum-Value)。权限本身已映射到数据库中的组/用户。因此,我看不到通过政策来解决这一问题的方法!
Gerwald '16

43
我和这些评论中的许多其他人一样,非常失望,因为对于Web API 2中可能使用的功能,使用属性进行授权已经变得非常绝望了,很抱歉,但是您的“需求”抽象无法涵盖我们以前可以使用的任何情况属性构造函数参数来通知基础授权算法。过去做类似这样的事情简直是脑筋急转弯[CustomAuthorize(Operator.And, Permission.GetUser, Permission.ModifyUser)]。我可以通过修改构造函数参数以多种方式使用单个自定义属性。
NathanAldenSr

61
我还感到震惊的是,自称“ ASP.NET安全负责人”的人实际上建议使用魔术字符串(破解的含义IAuthorizeData.Policy)和自定义策略提供程序来克服这种公然的监督,而不是在框架内解决它。我以为我们不应该创建自己的实现?除了让我们重新从头开始(再一次)实现授权之外,您几乎没有其他选择,而且这次甚至没有使用Web API的旧Authorize属性。现在,我们必须在操作过滤器或中间件级别上执行此操作。
NathanAldenSr

104

似乎在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;
        }
    }
}

4
因此,您只能使用它来拒绝授权,而不是授予它?
MEMark

1
@MEMark通过授予,您的意思是覆盖另一个授权属性?
gius

2
AFAIK,默认情况下允许访问,因此您需要明确拒绝它(例如,通过添加AuthorizeAttribute)。检查此问题以获取更多详细信息:stackoverflow.com/questions/17272422/…–
gius

16
还要注意,在建议的示例中,不必继承AuthorizeAttribute。您可以继承AttributeIAuthorizationFilter。这样,如果使用某些非标准的身份验证机制,您将不会获得以下异常:InvalidOperationException:未指定authenticationScheme,并且未找到DefaultChallengeScheme。
Anatolyevich

13
请注意,如果您的OnAuthorization实现需要等待异步方法,则应实现IAsyncAuthorizationFilter而不是IAuthorizationFilter其他方法,否则过滤器将同步执行,并且无论过滤器的结果如何,控制器动作都将执行。
Codemunkie

34

基于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());
    }
}

1
谢谢你 我创造了这个职位有一个稍微不同的实施和验证的请求stackoverflow.com/questions/49551047/...
安东Swanevelder

2
MumboJumboFunction <3
Marek Urbanowicz

31

您可以创建自己的AuthorizationHandler,以在Controllers和Actions上找到自定义属性,并将它们传递给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,该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
    }
}

1
我将尽快看一下。
NathanAldenSr

5
这是过度设计的方法...我使用一个简单的AuthorizationFilterAttribute来解决了同样的问题,但该方法接收了一个参数。您不需要为此进行思考,它似乎比“官方”解决方案(我觉得很差)更加巧妙。
Vi100

2
@ Vi100我在ASP.NET Core中找不到关于AuthorizationFilters的太多信息。官方文档页面说,他们目前正在研究此主题。 docs.microsoft.com/en-us/aspnet/core/security/authorization/…–
肖恩

4
@ Vi100如果我想知道有一种更简单的方法可以实现,请您分享您的解决方案。
肖恩

2
需要注意的一点是,上面的UnderlyingSystemType的使用无法编译,但是删除它似乎可以正常工作。
下午茶时间

25

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

简单:不要创建自己的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() { ... }
}

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

您到底想达到什么目标?


1
我在标题授权中收到一个会话ID。通过该ID,我将知道特定操作是否有效。
jltrem

1
那不是授权问题。我猜您的“会话ID”实际上是一个包含调用者身份的令牌:绝对应该在中间件级别进行。
凯文木屋

3
它不是身份验证(确定用户是谁),而是身份验证(确定用户是否应有权访问资源)。那么,您在哪里建议我寻求解决此问题?
jltrem

3
@jltrem,同意,您要说的是授权而不是身份验证。
bopapa_1979

2
@Pinpoint我不是。我查询该信息的另一个系统。该系统进行身份验证(确定用户)并进行授权(告诉我该用户可以访问的内容)。现在,我通过在每个控制器操作中调用一个方法让其他系统验证会话来使其工作。我想通过属性自动发生这种情况。
jltrem

4

如果有人只想使用当前的安全做法在授权阶段验证承载令牌,则可以,

将此添加到您的Startup / ConfigureServices

    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获取上下文。
JasonCoder

2

现代方式是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

@PiotrŚródka,欢迎您,请注意,答案有点“简化”,请测试文本中是否包含“:”,因为恶意用户可以通过在索引输出中不打好结尾来尝试使服务崩溃范围异常。一如既往地测试外部资源给您的内容
Walter Vehoeven

2

在撰写本文时,我相信可以使用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;
    }
}

要在Controller中使用此功能,只需[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呢?需要添加一些东西吗?
丹尼尔

正如其他地方所讨论的,是的。
不退款

1

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

例如,如果我们要检查登录的医生是否可以查看患者约会,则将“ 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()
    {

    }

1
请注意,当您要对SignalR中的集线器方法使用相同的属性时,IActionFilter将是一个问题。SignalR集线器期望IAuthorizationFilter
ilkerkaran

谢谢(你的)信息。我现在不在我的应用程序中使用SignalR,因此我没有对其进行测试。
阿卜杜拉

我猜是相同的原理,因为您仍然必须使用标题的授权条目,实现方式会有所不同
Walter Vehoeven

0

接受的答案(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); 
        } 
     }
}

请注意,上面的代码还可以使用户预先映射到数据存储中的策略。因此,在为用户编写索赔时,基本上可以直接或间接检索预先映射到用户的策略(例如,因为用户具有某个索赔值,并且该索赔值已被标识并映射到策略,例如(它也为具有该声明值的用户提供自动映射),并将策略注册为声明,以便在授权处理程序中,您可以简单地检查用户的声明中是否包含需求。索赔。那是为了满足策略要求的静态方式,例如,“名字”要求本质上是相当静态的。所以,

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

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

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

PS:我在手机上打了这个。请原谅任何错别字和缺乏格式。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.