我正在编写一个 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 来随后读取登录用户是谁。
从长远来看,降级到 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 内使用。此外,它不是两个因素,也不依赖于适当的加密,例如联合身份验证。