CoreWCF 基本身份验证 - .NET 6

问题描述 投票:0回答:2

我正在编写一个 CoreWCF PoC,我需要使用 HTTPS、BasicHttpBinding 和 Basic Authentication。

一切都运行良好,直到我尝试激活基本身份验证为止。因此,下面的代码使用将 ClientCredentialType 设置为 HttpClientCredentialType.Basic 的 Binding:

var basicHttpBinding = new BasicHttpBinding(BasicHttpSecurityMode.Transport);
basicHttpBinding.Security.Transport.ClientCredentialType = HttpClientCredentialType.Basic;

var app = builder.Build();
app.UseServiceModel(builder =>
{
     // Add service with a BasicHttpBinding at a specific endpoint
     builder.AddService<DownloadService>((serviceOptions) => {
         serviceOptions.DebugBehavior.IncludeExceptionDetailInFaults = true;
     }).AddServiceEndpoint<DownloadService, IDownloadService>(basicHttpBinding, "/DownloadService/basichttp");
});

启动时抛出异常: System.InvalidOperationException:“尝试激活“Microsoft.AspNetCore.Authentication.AuthenticationMiddleware”时无法解析类型“Microsoft.AspNetCore.Authentication.IAuthenticationSchemeProvider”的服务。”

知道如何在 CoreWCF 中设置 BasicAuthentication 来随后读取登录用户是谁。

asp.net-core authentication basic-authentication basichttpbinding corewcf
2个回答
3
投票

我已经重现了你提到的问题。我通过将 CoreWCF 包版本降级到 1.0.2 或 1.0.1 来解决这个问题。其他版本(> 1.0.2)也有此问题。


我的测试步骤


温馨提示:

降级这两个包的时候要注意顺序,具体顺序我忘记了,你可以试试,一定可以完成降级。


0
投票

从长远来看,降级到 CoreWCF 的早期版本并不是一个很好的解决方案,因为您将错过 CoreWCF 中的更新。

我在这里写了一篇如何使用 Core WCF 进行基本身份验证的文章: https://toreaurstad.blogspot.com/2023/11/implementing-basic-auth-in-core-wcf.html

请注意,这也依赖于 ASP.NET Core 管道,CoreWCF 和 ASP.NET Core 管道都必须设置为基本身份验证。

客户端存储库在这里: https://github.com/toreaurstadboss/CoreWCFWebClient1

服务器端存储库在这里: https://github.com/toreaurstadboss/CoreWCFService1

在客户端,我们有这个扩展方法来设置基本身份验证:

扩展方法WithBasicAuth:

BasicHttpBindingClientFactory.cs

using System.ServiceModel;
using System.ServiceModel.Channels;
namespace CoreWCFWebClient1.Extensions
{
    public static class BasicHttpBindingClientFactory
    {

        /// <summary>
        /// Creates a basic auth client with credentials set in header Authorization formatted as 'Basic [base64encoded username:password]'
        /// Makes it easier to perform basic auth in Asp.NET Core for WCF
        /// </summary>
        /// <param name="username"></param>
        /// <param name="password"></param>
        /// <returns></returns>
        public static TServiceImplementation WithBasicAuth<TServiceContract, TServiceImplementation>(this TServiceImplementation client, string username, string password)
              where TServiceContract : class
                where TServiceImplementation : ClientBase<TServiceContract>, new()
        {
            string clientUrl = client.Endpoint.Address.Uri.ToString();

            var binding = new BasicHttpsBinding();
            binding.Security.Mode = BasicHttpsSecurityMode.Transport;
            binding.Security.Transport.ClientCredentialType = HttpClientCredentialType.Basic;

            string basicHeaderValue = "Basic " + Base64Encode($"{username}:{password}");
            var eab = new EndpointAddressBuilder(new EndpointAddress(clientUrl));
            eab.Headers.Add(AddressHeader.CreateAddressHeader("Authorization",  // Header Name
                string.Empty,           // Namespace
                basicHeaderValue));  // Header Value
            var endpointAddress = eab.ToEndpointAddress();

            var clientWithConfiguredBasicAuth = (TServiceImplementation) Activator.CreateInstance(typeof(TServiceImplementation), binding, endpointAddress)!;
            clientWithConfiguredBasicAuth.ClientCredentials.UserName.UserName = username;
            clientWithConfiguredBasicAuth.ClientCredentials.UserName.Password = username;

            return clientWithConfiguredBasicAuth;
        }

        private static string Base64Encode(string plainText)
        {
            var plainTextBytes = System.Text.Encoding.UTF8.GetBytes(plainText);
            return Convert.ToBase64String(plainTextBytes);
        }

    }
}

接下来是如何在 asp.net core mvc razor 视图中使用此扩展方法:

@{

    string username = "someuser";
    string password = "somepassw0rd";

    var client = new ServiceClient().WithBasicAuth<IService, ServiceClient>(username, password);

    var result = await client.GetDataAsync(42);

    <h5>@Html.Raw(result)</h5>
}

请注意,这会在soap信封内设置一个带有配置的身份验证标头的BasicHttpsBinding。此外,我们还在 BasicHttpsBinding 上设置了 ClientCredentials,因为 CoreWCF 需要这样做。但是,凭据将从肥皂信封身份验证标头中读出。格式为: 'Basic [base64creds]',其中 [base64cred] 是用户名的 Base-64 编码字符串:密码凭据,不带方括号。

我已经在服务器端使用 CoreWCF 1.5.1 对此进行了测试。

wcf服务器端的csproj如下所示:

CoreWCFService1.csproj

<Project Sdk="Microsoft.NET.Sdk.Web">
  <PropertyGroup>
    <TargetFramework>net6.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>true</ImplicitUsings>
  </PropertyGroup>
  <ItemGroup>
    <Using Include="CoreWCF" />
    <Using Include="CoreWCF.Configuration" />
    <Using Include="CoreWCF.Channels" />
    <Using Include="CoreWCF.Description" />
    <Using Include="System.Runtime.Serialization " />
    <Using Include="CoreWCFService1" />
    <Using Include="Microsoft.Extensions.DependencyInjection.Extensions" />
  </ItemGroup>
  <ItemGroup>
    <PackageReference Include="CoreWCF.Primitives" Version="1.5.1" />
    <PackageReference Include="CoreWCF.Http" Version="1.5.1" />
  </ItemGroup>
</Project>

来自 Program.cs 的一些相关行用于设置基本身份验证:

程序.cs

 builder.Services.AddSingleton<IUserRepository, UserRepository>();

builder.Services.AddAuthentication("Basic").
            AddScheme<AuthenticationSchemeOptions, BasicAuthenticationHandler>
            ("Basic", null);
app.Use(async (context, next) =>
{
    // Only check for basic auth when path is for the TransportWithMessageCredential endpoint only
    if (context.Request.Path.StartsWithSegments("/Service.svc"))
    {
        // Check if currently authenticated
        var authResult = await context.AuthenticateAsync("Basic");
        if (authResult.None)
        {
            // If the client hasn't authenticated, send a challenge to the client and complete request
            await context.ChallengeAsync("Basic");
            return;
        }
    }
    // Call the next delegate/middleware in the pipeline.
    // Either the request was authenticated of it's for a path which doesn't require basic auth
    await next(context);
});

app.UseServiceModel(serviceBuilder =>
{
    var basicHttpBinding = new BasicHttpBinding();
    basicHttpBinding.Security.Mode = BasicHttpSecurityMode.Transport;
    basicHttpBinding.Security.Transport.ClientCredentialType = HttpClientCredentialType.Basic;
    serviceBuilder.AddService<Service>(options =>
    {
        options.DebugBehavior.IncludeExceptionDetailInFaults = true;
    });
    serviceBuilder.AddServiceEndpoint<Service, IService>(basicHttpBinding, "/Service.svc");

    var serviceMetadataBehavior = app.Services.GetRequiredService<ServiceMetadataBehavior>();
    serviceMetadataBehavior.HttpsGetEnabled = true;
});

查看上面提到的 Github 存储库以查看完整代码。

接下来是基本身份验证处理程序:

基本身份验证处理程序

using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.Options;
using System.Security.Claims;
using System.Security.Principal;
using System.Text;
using System.Text.Encodings.Web;

public class BasicAuthenticationHandler : AuthenticationHandler<AuthenticationSchemeOptions>
{

    private readonly IUserRepository _userRepository;
    public BasicAuthenticationHandler(IOptionsMonitor<AuthenticationSchemeOptions> options,
        ILoggerFactory logger,
        UrlEncoder encoder,
        ISystemClock clock, IUserRepository userRepository) :
       base(options, logger, encoder, clock)
    {
        _userRepository = userRepository;
    }

    protected async override Task<AuthenticateResult> HandleAuthenticateAsync()
    {
        string? authTicketFromSoapEnvelope = await Request!.GetAuthenticationHeaderFromSoapEnvelope();

        if (authTicketFromSoapEnvelope != null && authTicketFromSoapEnvelope.StartsWith("basic", StringComparison.OrdinalIgnoreCase))
        {
            var token = authTicketFromSoapEnvelope.Substring("Basic ".Length).Trim();
            var credentialsAsEncodedString = Encoding.UTF8.GetString(Convert.FromBase64String(token));
            var credentials = credentialsAsEncodedString.Split(':');
            if (await _userRepository.Authenticate(credentials[0], credentials[1]))
            {
                var identity = new GenericIdentity(credentials[0]);
                var claimsPrincipal = new ClaimsPrincipal(identity);
                var ticket = new AuthenticationTicket(claimsPrincipal, Scheme.Name);
                return await Task.FromResult(AuthenticateResult.Success(ticket));
            }
        }

        return await Task.FromResult(AuthenticateResult.Fail("Invalid Authorization Header"));
    }

    protected override Task HandleChallengeAsync(AuthenticationProperties properties)
    {
        Response.StatusCode = 401;
        Response.Headers.Add("WWW-Authenticate", "Basic realm=\"thoushaltnotpass.com\"");
        Context.Response.WriteAsync("You are not logged in via Basic auth").Wait();
        return Task.CompletedTask;
    }

}

用户存储库如下所示:

public interface IUserRepository
    {

        public Task<bool> Authenticate(string username, string password);
    }

    public class UserRepository : IUserRepository
    {
        public Task<bool> Authenticate(string username, string password)
        {
            //TODO: some dummie auth mechanism used here, make something more realistic such as DB user repo lookup or similar
            if (username == "someuser" && password == "somepassw0rd")
            {
                return Task.FromResult(true);
            }
            return Task.FromResult(false);
        }
    }

服务看起来像这样,注意[Authorize]属性的使用:

public class Service : IService
 {

     [Authorize]
     public string GetData(int value)
     {
         return $"You entered: {value}. <br />The client logged in with transport security with BasicAuth with https (BasicHttpsBinding).<br /><br />The username is set inside ServiceSecurityContext.Current.PrimaryIdentity.Name: {ServiceSecurityContext.Current.PrimaryIdentity.Name}. <br /> This username is also stored inside Thread.CurrentPrincipal.Identity.Name: {Thread.CurrentPrincipal?.Identity?.Name}";
     }

     public CompositeType GetDataUsingDataContract(CompositeType composite)
     {
         if (composite == null)
         {
             throw new ArgumentNullException("composite");
         }
         if (composite.BoolValue)
         {
             composite.StringValue += "Suffix";
         }
         return composite;
     }
 }

这里是HttpRequestExtensions中读取soap授权头的辅助方法,注意BodyReader和AdvanceTo的使用,它必须是用于读取Request并在读取后回滚Request的方法。

HttpRequestExtensions.cs

   using System.IO.Pipelines;
    using System.Text;
    using System.Xml.Linq;
    
    public static class HttpRequestExtensions
    {
    
        public static async Task<string?> GetAuthenticationHeaderFromSoapEnvelope(this HttpRequest request)
        {
            ReadResult requestBodyInBytes = await request.BodyReader.ReadAsync();
            string body = Encoding.UTF8.GetString(requestBodyInBytes.Buffer.FirstSpan);
            request.BodyReader.AdvanceTo(requestBodyInBytes.Buffer.Start, requestBodyInBytes.Buffer.End);
    
            string authTicketFromHeader = null;
    
            if (body?.Contains(@"http://schemas.xmlsoap.org/soap/envelope/") == true)
            {
                XNamespace ns = "http://schemas.xmlsoap.org/soap/envelope/";
                var soapEnvelope = XDocument.Parse(body);
                var headers = soapEnvelope.Descendants(ns + "Header").ToList();
    
                foreach (var header in headers)
                {
                    var authorizationElement = header.Element("Authorization");
                    if (!string.IsNullOrWhiteSpace(authorizationElement?.Value))
                    {
                        authTicketFromHeader = authorizationElement.Value;
                        break;
                    }
                }
            }
    
            return authTicketFromHeader;
        }
    
    } 

这可行,但 Authentication.Fail 遗憾的是给出 500 内部服务器错误而不是 401,我还没有弄清楚为什么会发生这种情况。但此屏幕截图显示了登录的用户:

[![通过核心 WCF 中的基本身份验证传输级别安全性显示登录用户][1]][1] [1]:https://i.stack.imgur.com/Z4SJx.png

这里显示的代码当然应该进行改进,并且需要一些技巧才能使其正常工作。遗憾的是,我在 CoreWCF 中找不到任何关于此的好的教程。这也许表明 CoreWCF 对于某些场景来说还有些早期,尽管 CoreWCF 有其他身份验证机制并且对这些机制有更好的支持。基本身份验证不被认为是非常安全的身份验证机制,应始终在 HTTPS 内使用。此外,它不是两个因素,也不依赖于适当的加密,例如联合身份验证。

© www.soinside.com 2019 - 2024. All rights reserved.