具有多个范围的 ASP.NET Core RequireClaim“范围”

Posted

技术标签:

【中文标题】具有多个范围的 ASP.NET Core RequireClaim“范围”【英文标题】:ASP.NET Core RequireClaim "scope" with multiple scopes 【发布时间】:2019-07-18 00:34:47 【问题描述】:

我正在 ASP.NET Core 2.1 上开发一个授权系统,该系统在授予访问权限之前需要资源级别和范围遵守。也就是说,我必须是一本书的作者(可以有多个),并且必须具有必要的范围(“write.book”、“read.book”、“delete.book”等)。我在Startup.cs 中成功配置了JWT,并在传递无效令牌时收到401s。我遇到的问题是执行范围。 policy.RequireClaim("scope", "write.book") 在访问令牌只需要一个必需的范围时起作用,但访问令牌包含多个范围 "write.book delete.book" 总是失败。如何配置策略以要求范围列表可能是访问令牌包含的范围的子集?我没有看到任何接受范围列表的Policy 方法,所以我假设框架只是在执行字符串比较,这就是授权失败的原因。 write.book != write.book delete.book。澄清一下,如果策略只需要一个范围write.book,但访问令牌中存在多个write.book delete.book,则授权失败。

以下代码仅在访问令牌包含一个范围时有效,如果存在多个范围则失败。

authorization.AddPolicy("writeBookPolicy", policy => 
    policy.RequireAuthenticatedUser().AddAuthenticationSchemes("Bearer")
      .RequireClaim("scope", "write.book").Build();
);

"scope": "write.book" // Works
"scope": "write.book delete.book" //Fails

【问题讨论】:

您是否尝试过两次指定RequireClaimRequireClaim("scope", "write.book").RequireClaim("scope", "delete.book") ? .RequireClaim("scope", "write.book", "delete.book") 怎么样? 问题是端点只需要“write.book”,但访问令牌包含“write.book”和“delete.book”。访问令牌中同时包含两个范围是导致授权失败的原因。 你如何设置令牌的范围?每个范围都应定义为自己的属性:"scope": "write.book", "scope": "delete.book" 我遇到了来自 Auth0 的教程,他们在其中开发了一个 RequireHandler 来拆分空格分隔的范围。我认为 ASP.NET Core 会有一个内置的解决方案。 auth0.com/docs/quickstart/backend/aspnet-core-webapi/… 【参考方案1】:

您必须使用更复杂的索赔检查。改用RequireAssertion() 并解析出范围声明:

var scopes = new[]  "write.book", "delete.book" ;
builder.RequireAssertion(context => 
   var claim = context.User.FindFirst("scope");
   if(claim == null)  return false; 
   return claim.Value.Split(' ').Any(s =>
      scopes.Contains(s, StringComparer.OrdinalIgnoreCase)
   );
);

我写了一个小扩展方法,虽然不太容易阅读,但更容易使用:

private static readonly IEnumerable<string> _scopeClaimTypes = new HashSet<string>(StringComparer.OrdinalIgnoreCase) 
    "http://schemas.microsoft.com/identity/claims/scope",
    "scope"
;
public static AuthorizationPolicyBuilder RequireScope(this AuthorizationPolicyBuilder builder, params string[] scopes) 
    return builder.RequireAssertion(context =>
        context.User
            .Claims
            .Where(c => _scopeClaimTypes.Contains(c.Type))
            .SelectMany(c => c.Value.Split(' '))
            .Any(s => scopes.Contains(s, StringComparer.OrdinalIgnoreCase))
    );

【讨论】:

【参考方案2】:

我创建的这些扩展方法允许您在需要所有作用域或任何作用域的地方添加作用域策略

    using Microsoft.AspNetCore.Authorization;
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Security.Claims;

    public static class AuthorizationPolicyBuilder_Extentions
    
        public static void AddPolicyScope_AllowedScopes(this AuthorizationOptions authorizationOptions, string policyName, params string[] allowedScopes) =>
            authorizationOptions.AddPolicy(policyName, policyBuilder => policyBuilder.RequireClaim("scope", allowedScopes));


        public static void AddPolicyScope_AllRequiredScopes(this AuthorizationOptions authorizationOptions, string policyName, params string[] requiredScopes) =>
            authorizationOptions.AddPolicy(policyName, policyBuilder => policyBuilder.RequireScopes(requiredScopes));

        private static AuthorizationPolicyBuilder RequireScopes(this AuthorizationPolicyBuilder builder, params string[] requiredScopes) =>
             builder.RequireAssertion(context => 
                 var userScopes = GetUserScopes(context);
                 return requiredScopes.All(scope => userScopes.Contains(scope, StringComparer.CurrentCulture));
             );

        private static IEnumerable<string> GetUserScopes(this AuthorizationHandlerContext context) =>
            context?.User?
                    .Claims
                    .Where(c => c.Type.Equals("scope"))
                    .SelectMany(c => c.Value.Split(' ')) ?? new List<string>();
    

【讨论】:

【参考方案3】:

另一种选择是将范围拆分为多个声明

        services.AddAuthentication("Bearer")
            .AddJwtBearer("Bearer", options =>
            
                options.Authority = "https://localhost:5001";
                options.TokenValidationParameters = new()  ValidateAudience = false ;

                options.Events = new Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerEvents()
                
                    OnTokenValidated = async (context) =>
                    
                        if (context.Principal?.Identity is ClaimsIdentity claimsIdentity)
                        
                            var scopeClaims = claimsIdentity.FindFirst("scope");
                            if (scopeClaims is not null)
                            
                                claimsIdentity.RemoveClaim(scopeClaims);
                                claimsIdentity.AddClaims(scopeClaims.Value.Split(' ').Select(scope => new Claim("scope", scope)));
                            
                        
                        await Task.CompletedTask;
                    
                ;
            );

【讨论】:

以上是关于具有多个范围的 ASP.NET Core RequireClaim“范围”的主要内容,如果未能解决你的问题,请参考以下文章

具有多个路由参数的 ASP.NET Core Razor 页面层次结构

如何在 asp.net core 2.0 中允许具有不同发行者的多个 JWT

在具有多个数组列表<>的 ASP.Net Core 应用程序上显示 SQL 数据 [重复]

如何在asp.net core中添加多个身份和多个角色

在 ASP.NET Core 应用程序中使用多个 HttpClient 对象

System.Web 可以与具有完整框架的 ASP.Net Core 一起使用吗