在 Blazor 中对客户端和服务器使用相同的授权策略

Use same authorization policies for client and server in Blazor

这个问题是我报告的 ASP.NET Core hosted Blazor 问题的后续问题 here

使用 .NET Core 3.0 的当前预览版 (6),ASP.NET Core hosted Blazor 项目模板创建以下三个项目:

我想在我的服务器项目(以保护端点)以及我的客户端项目(以动态显示或隐藏视图或部分视图)中启用授权。

对于那个用例,我在 MyProject.Shared 项目中创建了策略,之后我可以在客户端项目中使用它们。但是,它们在服务器项目中不可用。

问题:

    • .NET Core 3.0 授权策略源自 Microsoft.AspNetCore.Authorization.AuthorizationPolicy,由 <FrameworkReference Include="Microsoft.AspNetCore.App" />.
    • 定义
    • .NET Standard 2.0 授权策略源自 <PackageReference Include="Microsoft.AspNetCore.Authorization" [...] /> 定义的 Microsoft.AspNetCore.Authorization.AuthorizationPolicy
    • AuthorizationPolicy 构造函数需要一组身份验证方案。
    • 对于 .NET Core 3.0,它们在 Microsoft.AspNetCore.Identity.IdentityConstants 中定义,由 <FrameworkReference Include="Microsoft.AspNetCore.App" />.
    • 定义
    • 对于 .NET Standard 2.0,它们在 Microsoft.AspNetCore.Identity.IdentityConstants 中定义,由 <PackageReference Include="Microsoft.AspNetCore.Identity" Version="2.2.0" /> 定义。但是,此包不会在任何高于 2.2.0 的版本中可用,因为 .NET Core 3.0 版本 Microsoft decided to discontinue certain packages,而是将那些 类 移动到 <FrameworkReference Include="Microsoft.AspNetCore.App" /> .

我的问题:
在服务器和客户端项目中使用 相同 策略的正确方法是什么 - 无需为 .NET Standard 和 .NET Core 定义两次?


编辑: 为了在 MyProject.Shared 项目中正确设置共享策略,我必须引用 Microsoft.AspNetCore.Identity (2.2.0) 包 - 包括 IdentityConstants.ApplicationScheme。 但是,这会导致 MyProject.Client 的构建失败:

2>Processing embedded resource linker descriptor: mscorlib.xml
2>Duplicate preserve in resource mscorlib.xml in mscorlib, Version=2.0.5.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e of System.Threading.WasmRuntime (All).  Duplicate uses (All)
2>Type Mono.ValueTuple has no fields to preserve
2>Type System.Reflection.Assembly has no fields to preserve
2>Fatal error in IL Linker
2>
2>Unhandled Exception: Mono.Linker.MarkException: Error processing method: 'System.Void System.Security.Permissions.PrincipalPermission::Demand()' in assembly: 'System.Security.Permissions.dll' ---> Mono.Cecil.ResolutionException: Failed to resolve System.Security.Principal.IPrincipal System.Threading.Thread::get_CurrentPrincipal()
2>   at Mono.Linker.Steps.MarkStep.HandleUnresolvedMethod(MethodReference reference)
2>   at Mono.Linker.Steps.MarkStep.MarkMethod(MethodReference reference)
2>   at Mono.Linker.Steps.MarkStep.MarkInstruction(Instruction instruction)
2>   at Mono.Linker.Steps.MarkStep.MarkMethodBody(MethodBody body)
2>   at Mono.Linker.Steps.MarkStep.ProcessMethod(MethodDefinition method)
2>   at Mono.Linker.Steps.MarkStep.ProcessQueue()
2>   --- End of inner exception stack trace ---
2>   at Mono.Linker.Steps.MarkStep.ProcessQueue()
2>   at Mono.Linker.Steps.MarkStep.ProcessPrimaryQueue()
2>   at Mono.Linker.Steps.MarkStep.Process()
2>   at Mono.Linker.Steps.MarkStep.Process(LinkContext context)
2>   at Mono.Linker.Pipeline.ProcessStep(LinkContext context, IStep step)
2>   at Mono.Linker.Pipeline.Process(LinkContext context)
2>   at Mono.Linker.Driver.Run(ILogger customLogger)
2>   at Mono.Linker.Driver.Execute(String[] args, ILogger customLogger)
2>   at Mono.Linker.Driver.Main(String[] args)

接着是这个神秘的错误消息:

\.nuget\packages\microsoft.aspnetcore.blazor.build.0.0-preview6.19307.2\targets\Blazor.MonoRuntime.targets(439,5): error MSB3073: The command "dotnet "C:\Users\Stefan\.nuget\packages\microsoft.aspnetcore.blazor.mono[=11=].10.0-preview6.19303.4\build\netstandard1.0\../../tools/illink/illink.dll" -l none --disable-opt unreachablebodies --verbose --strip-security true --exclude-feature com --exclude-feature sre -v false -c link -u link -b true -d "C:\Users\Stefan\.nuget\packages\microsoft.aspnetcore.blazor.mono[=11=].10.0-preview6.19303.4\build\netstandard1.0\../../tools/mono/bcl/" -d "C:\Users\Stefan\.nuget\packages\microsoft.aspnetcore.blazor.mono[=11=].10.0-preview6.19303.4\build\netstandard1.0\../../tools/mono/bcl/Facades/" -o "S:\git\hou-units\src\UNITS\UNITS.Client\obj\Debug\netstandard2.0\blazor\linker/" -x "C:\Users\Stefan\.nuget\packages\microsoft.aspnetcore.blazor.build.0.0-preview6.19307.2\targets\BuiltInBclLinkerDescriptor.xml" -x "S:\git\hou-units\src\UNITS\UNITS.Client\obj\Debug\netstandard2.0\blazor\linker.descriptor.xml" -a "C:\Users\Stefan\.nuget\packages\jetbrains.annotations19.1.3\lib\netstandard2.0\JetBrains.Annotations.dll" -a "C:\Program Files\dotnet\sdk\NuGetFallbackFolder\microsoft.aspnetcore.authentication.2.0\lib\netstandard2.0\Microsoft.AspNetCore.Authentication.dll" -a "C:\Program Files\dotnet\sdk\NuGetFallbackFolder\microsoft.aspnetcore.authentication.abstractions.2.0\lib\netstandard2.0\Microsoft.AspNetCore.Authentication.Abstractions.dll" -a "C:\Program Files\dotnet\sdk\NuGetFallbackFolder\microsoft.aspnetcore.authentication.cookies.2.0\lib\netstandard2.0\Microsoft.AspNetCore.Authentication.Cookies.dll" -a "C:\Program Files\dotnet\sdk\NuGetFallbackFolder\microsoft.aspnetcore.authentication.core.2.0\lib\netstandard2.0\Microsoft.AspNetCore.Authentication.Core.dll" -a "C:\Users\Stefan\.nuget\packages\microsoft.aspnetcore.authorization.0.0-preview6.19307.2\lib\netstandard2.0\Microsoft.AspNetCore.Authorization.dll" -a "C:\Users\Stefan\.nuget\packages\microsoft.aspnetcore.blazor.0.0-preview6.19307.2\lib\netstandard2.0\Microsoft.AspNetCore.Blazor.dll" -a "C:\Users\Stefan\.nuget\packages\microsoft.aspnetcore.components.0.0-preview6.19307.2\lib\netstandard2.0\Microsoft.AspNetCore.Components.dll" -a "C:\Users\Stefan\.nuget\packages\microsoft.aspnetcore.components.browser.0.0-preview6.19307.2\lib\netstandard2.0\Microsoft.AspNetCore.Components.Browser.dll" -a "C:\Program Files\dotnet\sdk\NuGetFallbackFolder\microsoft.aspnetcore.cryptography.internal.2.0\lib\netstandard2.0\Microsoft.AspNetCore.Cryptography.Internal.dll" -a "C:\Program Files\dotnet\sdk\NuGetFallbackFolder\microsoft.aspnetcore.cryptography.keyderivation.2.0\lib\netstandard2.0\Microsoft.AspNetCore.Cryptography.KeyDerivation.dll" -a "C:\Program Files\dotnet\sdk\NuGetFallbackFolder\microsoft.aspnetcore.dataprotection.2.0\lib\netstandard2.0\Microsoft.AspNetCore.DataProtection.dll" -a "C:\Program Files\dotnet\sdk\NuGetFallbackFolder\microsoft.aspnetcore.dataprotection.abstractions.2.0\lib\netstandard2.0\Microsoft.AspNetCore.DataProtection.Abstractions.dll" -a "C:\Program Files\dotnet\sdk\NuGetFallbackFolder\microsoft.aspnetcore.hosting.abstractions.2.0\lib\netstandard2.0\Microsoft.AspNetCore.Hosting.Abstractions.dll" -a "C:\Program Files\dotnet\sdk\NuGetFallbackFolder\microsoft.aspnetcore.hosting.server.abstractions.2.0\lib\netstandard2.0\Microsoft.AspNetCore.Hosting.Server.Abstractions.dll" -a "C:\Program Files\dotnet\sdk\NuGetFallbackFolder\microsoft.aspnetcore.http.2.0\lib\netstandard2.0\Microsoft.AspNetCore.Http.dll" -a "C:\Program Files\dotnet\sdk\NuGetFallbackFolder\microsoft.aspnetcore.http.abstractions.2.0\lib\netstandard2.0\Microsoft.AspNetCore.Http.Abstractions.dll" -a "C:\Program Files\dotnet\sdk\NuGetFallbackFolder\microsoft.aspnetcore.http.extensions.2.0\lib\netstandard2.0\Microsoft.AspNetCore.Http.Extensions.dll" -a "C:\Program Files\dotnet\sdk\NuGetFallbackFolder\microsoft.aspnetcore.http.features.2.0\lib\netstandard2.0\Microsoft.AspNetCore.Http.Features.dll" -a "C:\Program Files\dotnet\sdk\NuGetFallbackFolder\microsoft.aspnetcore.identity.2.0\lib\netstandard2.0\Microsoft.AspNetCore.Identity.dll" -a "C:\Users\Stefan\.nuget\packages\microsoft.aspnetcore.metadata.0.0-preview6.19307.2\lib\netstandard2.0\Microsoft.AspNetCore.Metadata.dll" -a "C:\Program Files\dotnet\sdk\NuGetFallbackFolder\microsoft.aspnetcore.webutilities.2.0\lib\netstandard2.0\Microsoft.AspNetCore.WebUtilities.dll" -a "C:\Users\Stefan\.nuget\packages\microsoft.extensions.configuration.2.0\lib\netstandard2.0\Microsoft.Extensions.Configuration.dll" -a "C:\Users\Stefan\.nuget\packages\microsoft.extensions.configuration.abstractions.2.0\lib\netstandard2.0\Microsoft.Extensions.Configuration.Abstractions.dll" -a "C:\Users\Stefan\.nuget\packages\microsoft.extensions.configuration.binder.2.0\lib\netstandard2.0\Microsoft.Extensions.Configuration.Binder.dll" -a "C:\Users\Stefan\.nuget\packages\microsoft.extensions.dependencyinjection.0.0-preview6.19304.6\lib\netstandard2.0\Microsoft.Extensions.DependencyInjection.dll" -a "C:\Users\Stefan\.nuget\packages\microsoft.extensions.dependencyinjection.abstractions.0.0-preview6.19304.6\lib\netstandard2.0\Microsoft.Extensions.DependencyInjection.Abstractions.dll" -a "C:\Program Files\dotnet\sdk\NuGetFallbackFolder\microsoft.extensions.fileproviders.abstractions.2.0\lib\netstandard2.0\Microsoft.Extensions.FileProviders.Abstractions.dll" -a "C:\Program Files\dotnet\sdk\NuGetFallbackFolder\microsoft.extensions.hosting.abstractions.2.0\lib\netstandard2.0\Microsoft.Extensions.Hosting.Abstractions.dll" -a "C:\Program Files\dotnet\sdk\NuGetFallbackFolder\microsoft.extensions.identity.core.2.0\lib\netstandard2.0\Microsoft.Extensions.Identity.Core.dll" -a "C:\Users\Stefan\.nuget\packages\microsoft.extensions.logging.2.0\lib\netstandard2.0\Microsoft.Extensions.Logging.dll" -a "C:\Users\Stefan\.nuget\packages\microsoft.extensions.logging.abstractions.0.0-preview6.19304.6\lib\netstandard2.0\Microsoft.Extensions.Logging.Abstractions.dll" -a "C:\Program Files\dotnet\sdk\NuGetFallbackFolder\microsoft.extensions.objectpool.2.0\lib\netstandard2.0\Microsoft.Extensions.ObjectPool.dll" -a "C:\Users\Stefan\.nuget\packages\microsoft.extensions.options.0.0-preview6.19304.6\lib\netstandard2.0\Microsoft.Extensions.Options.dll" -a "C:\Users\Stefan\.nuget\packages\microsoft.extensions.primitives.0.0-preview6.19304.6\lib\netstandard2.0\Microsoft.Extensions.Primitives.dll" -a "C:\Program Files\dotnet\sdk\NuGetFallbackFolder\microsoft.extensions.webencoders.2.0\lib\netstandard2.0\Microsoft.Extensions.WebEncoders.dll" -a "C:\Users\Stefan\.nuget\packages\microsoft.jsinterop.0.0-preview6.19304.6\lib\netstandard2.0\Microsoft.JSInterop.dll" -a "C:\Program Files\dotnet\sdk\NuGetFallbackFolder\microsoft.net.http.headers.2.0\lib\netstandard2.0\Microsoft.Net.Http.Headers.dll" -a "C:\Program Files\dotnet\sdk\NuGetFallbackFolder\microsoft.win32.registry.5.0\lib\netstandard2.0\Microsoft.Win32.Registry.dll" -a "C:\Users\Stefan\.nuget\packages\mono.webassembly.interop.0.0-preview6.19304.6\lib\netstandard2.0\Mono.WebAssembly.Interop.dll" -a "C:\Users\Stefan\.nuget\packages\newtonsoft.json.0.2\lib\netstandard2.0\Newtonsoft.Json.dll" -a "C:\Users\Stefan\.nuget\packages\system.buffers.5.0\lib\netstandard2.0\System.Buffers.dll" -a "C:\Users\Stefan\.nuget\packages\system.componentmodel.annotations.6.0-preview6.19303.8\lib\netstandard2.0\System.ComponentModel.Annotations.dll" -a "C:\Users\Stefan\.nuget\packages\system.memory.5.3\lib\netstandard2.0\System.Memory.dll" -a "C:\Program Files\dotnet\sdk\NuGetFallbackFolder\system.numerics.vectors.5.0\lib\netstandard2.0\System.Numerics.Vectors.dll" -a "C:\Users\Stefan\.nuget\packages\system.runtime.compilerservices.unsafe.6.0-preview6.19303.8\lib\netstandard2.0\System.Runtime.CompilerServices.Unsafe.dll" -a "C:\Program Files\dotnet\sdk\NuGetFallbackFolder\system.security.accesscontrol.5.0\lib\netstandard2.0\System.Security.AccessControl.dll" -a "C:\Users\Stefan\.nuget\packages\system.security.cryptography.cng.4.0\lib\netstandard2.0\System.Security.Cryptography.Cng.dll" -a "C:\Program Files\dotnet\sdk\NuGetFallbackFolder\system.security.cryptography.pkcs.5.0\lib\netstandard2.0\System.Security.Cryptography.Pkcs.dll" -a "C:\Program Files\dotnet\sdk\NuGetFallbackFolder\system.security.cryptography.xml.5.0\lib\netstandard2.0\System.Security.Cryptography.Xml.dll" -a "C:\Program Files\dotnet\sdk\NuGetFallbackFolder\system.security.permissions.5.0\lib\netstandard2.0\System.Security.Permissions.dll" -a "C:\Program Files\dotnet\sdk\NuGetFallbackFolder\system.security.principal.windows.5.0\lib\netstandard2.0\System.Security.Principal.Windows.dll" -a "C:\Program Files\dotnet\sdk\NuGetFallbackFolder\system.text.encodings.web.5.0\lib\netstandard2.0\System.Text.Encodings.Web.dll" -a "C:\Users\Stefan\.nuget\packages\system.text.json.6.0-preview6.19303.8\lib\netstandard2.0\System.Text.Json.dll" -a "C:\Users\Stefan\.nuget\packages\system.threading.tasks.extensions.5.2\lib\netstandard2.0\System.Threading.Tasks.Extensions.dll" -a "S:\git\hou-units\src\UNITS\UNITS.Shared\bin\Debug\netstandard2.0\UNITS.Shared.dll" -a "S:\git\hou-units\src\UNITS\UNITS.Client\obj\Debug\netstandard2.0\UNITS.Client.dll"" exited with code -532462766.

可以找到示例项目on GitHub

我建议您使用 Blazor 框架中内置的身份验证和授权系统。参见 https://github.com/aspnet/AspNetCore/tree/master/src/Components/Components/src/Auth

您可以使用支持policy-based授权的组件

示例:server-side 或 client-side Blazor

<AuthorizeView Policy="content-editor">
    You can only see this if you satify the "content-editor" policy.
</AuthorizeView>

使用[授权]属性

@page "/"
@attribute [Authorize(Policy = "content-editor")]

You can only see this if you satisfy the 'content-editor' policy.

上面这两个代码片段可以满足您对dynamically show or hide views or parts of views

的需求

当然,您需要在您的服务器上执行授权检查,以保护您的 client-side 应用程序访问的 API 端点

在文档中查找更多详细信息和示例。这是一个了不起的系统,您可以使用它轻松地验证和授权您的用户。

希望这对您有所帮助...

阅读 Chris Sainty's recent article about policy-based authorization 后,我想出的正确方法是从类型化策略切换到使用其他重载,这允许配置策略。

我没有为每个需要 AuthenticationScheme 的策略创建 class 并使用 AuthorizationOptions.AddPolicy(string name, AuthorizationPolicy policy) 方法,而是实现了一个调用 AuthorizationOptions.AddPolicy(string name, Action<AuthorizationPolicyBuilder> configurePolicy) 的扩展方法。

先决条件

  • 使用 ASP.NET Core 托管的 Blazor 模板。
  • 在您的共享项目中,您必须引用版本大于或等于 3.0.0 的 NuGet 包 Microsoft.AspNetCore.Authorization

1。定义政策

在共享项目中,创建一个名为 Policies 的 class,您可以在其中使用配置方法定义所有策略。配置方法应具有您的策略名称 - 例如:

public static class Policies
{
    // This policy is internal and required for the extension method explained further below.
    // You CANNOT use this policy in your controllers or pages.
    internal static void PolicyConfigurationFailedFallback(AuthorizationPolicyBuilder builder)
        => builder.RequireAssertion(context => false);

    public static void TimeMustBeEvening(AuthorizationPolicyBuilder builder)
        => builder.RequireAuthenticatedUser()
                  .RequireAssertion(context => DateTime.UtcNow.Hour >= 18);
}

2。创建扩展方法

此扩展方法自动将您 Policies class 中的所有 public 策略应用到 services.AddAuthorizationCore 方法(参见步骤 #3):

public static class AuthorizationOptionsExtension
{
    public static void AddSharedPolicies(this AuthorizationOptions options,
                                         ILogger logger)
    {
        logger.LogInformation($"{nameof(AuthorizationPolicy)} Configuration started ...");
        var policies = FindPolicies();
        options.TryToAddPolicies(policies, logger);
        logger.LogInformation($"{nameof(AuthorizationPolicy)} Configuration completed.");
    }

    private static IEnumerable<PolicyInformation> FindPolicies()
    {
        var policyProvider = typeof(Policies);
        return from method in policyProvider.GetMethods(BindingFlags.Public | BindingFlags.Static)
               // The method should configure the policy builder, not return a built policy.
               where method.ReturnType == typeof(void)
               let methodParameter = method.GetParameters()
               // The method has to accept the AuthorizationPolicyBuilder, and no other parameter.
               where methodParameter.Length == 1 && methodParameter[0].ParameterType == typeof(AuthorizationPolicyBuilder)
               select new PolicyInformation(method.Name, method);
    }

    private static void TryToAddPolicies(this AuthorizationOptions options,
                                         IEnumerable<PolicyInformation> policies,
                                         ILogger logger)
    {
        foreach (var policy in policies)
        {
            try
            {
                options.AddPolicy(policy.Name, builder => { policy.Method.Invoke(null, new object[] {builder}); });
                logger.LogInformation($"Policy '{policy.Name}' was configured successfully.");
            }
            catch (Exception e)
            {
                options.AddPolicy(policy.Name, Policies.PolicyConfigurationFailedFallback);
                logger.LogCritical(e, $"Failed to configure policy '{policy.Name}'. Using fallback policy instead.");
            }
        }
    }

    private class PolicyInformation
    {
        internal string Name { get; }

        internal MethodInfo Method { get; }

        internal PolicyInformation(string name,
                                   MethodInfo method)
        {
            Name = name;
            Method = method;
        }
    }
}

第 1 步中的 PolicyConfigurationFailedFallback-policy 将阻止访问您的受保护资源,同时仍为您预期的策略名称提供策略。这样,您就不会最终将浏览器或 Visual Studio 控制台与 "policy not found" 消息聚集在一起。

当然,如果您愿意,您可以随时删除日志记录。

3。在客户端和服务器中配置服务

利用扩展方法,Startupclass中的ConfigureServices方法(客户端和服务器) 缩小得很:

public void ConfigureServices(IServiceCollection services)
{
    /* ... */

    // For the server-side Startup class you can get the ILogger from the constructor instead.
    // For the client-side Startup class you have to get the ILogger here.
    var logger = services.BuildServiceProvider().GetService<ILogger<Startup>>();
    services.AddAuthorizationCore(options => options.AddSharedPolicies(logger));

    /* ... */
}

4。在控制器中使用策略

直截了当,而不是定义另一个包含您的策略名称的 属性,您可以简单地使用 nameof() 运算符来获取您的策略名称:

[Authorize(nameof(Policies.TimeMustBeEvening))]
[Route("api/[controller]")]
public class SampleDataController : Controller
{
   /* ... */
}

5。在 Blazor 页面和组件中使用策略

与将策略应用到控制器的方式类似,您可以在 Blazor 页面和组件中使用 nameof() 运算符。

对于整个 Blazor 页面:

@page "/fetchdata"
@attribute [Authorize(Policy = nameof(Policies.NameMustBeBlazorAdmin))]

或使用 AuthorizeView 组件的个别部分:

<AuthorizeView Policy="@nameof(Policies.NameMustBeBlazorAdmin)">
<th>Temp. (F)</th>
<th>Summary</th>
</AuthorizeView>