如何解决如何使用 OData 身份验证 (OData Client V7)?
如何在 Microsoft (Microsoft.OData.Client
) 的 OData 客户端中使用 Kerberos/NTLM 身份验证(就像在 HttpClient 中一样)?
我正在使用包 Microsoft.OData.Client 7.9.0
并且我正在尝试连接到启用了 https 和身份验证的 OData 端点。但是我无法检索任何数据,而是抛出此异常:
Microsoft.OData.Edm.Csdl.EdmParseException: "Encountered the following errors when parsing the CSDL document:
XmlError : Root element is missing. : (0,0)"
由于缺乏权限,上下文似乎无法找到请求的资源。这是参考实现:
// Simple data class
public class Person
{
public string Id { get; set; }
public string UserName { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
}
// OData service context
public class Container : DataServiceContext
{
public DataServiceQuery<Person> People { get; }
public Container(Uri serviceRoot) : base(serviceRoot)
{
People = base.CreateQuery<Person>(nameof(People));
// This is not working ...
Credentials = new NetworkCredential("user",@"p@ssw0rd!");
}
}
Container context = new Container(new Uri("https://targetservice.dev/ODataV4/$metadata"));
var result = context.People.Execute() as QueryOperationResponse<Person>;
向 Credentials
属性提供凭据在这里似乎没有任何影响。
解决方法
经过一段时间的研究和测试,我设法让它发挥了作用。由于缺乏权限,确实无法请求请求的资源 (CSDL)。但是最初可以使用 HttpClient 请求 CSDL,这会考虑提供的凭据:
public class Container : DataServiceContext
{
public override ICredentials Credentials { get => base.Credentials; set => throw new NotSupportedException(); }
public bool UseDefaultCredentials { get; }
public DataServiceQuery<Person> People { get; }
private static IEdmModel? ParsedModel;
public Container(Uri serviceRoot,ICredentials? credentials = null,bool useDefaultCredentials = false) : base(serviceRoot)
{
if (serviceRoot is null) throw new ArgumentNullException(nameof(serviceRoot));
base.Credentials = useDefaultCredentials ? credentials ?? CredentialCache.DefaultCredentials : credentials;
UseDefaultCredentials = useDefaultCredentials;
People = base.CreateQuery<Person>(nameof(People));
// It is required to load the service model initially.
Format.LoadServiceModel = () => RequestModel();
Format.UseJson();
}
// This method requets the service model directly from the OData endpoint via HttpClient.
// It also uses the supplied credentials.
private IEdmModel RequestModel()
{
if (ParsedModel is not null) return ParsedModel;
// Create http client (+ handler) populated with credentials.
using HttpClientHandler clientHandler = new()
{
Credentials = UseDefaultCredentials ? CredentialCache.DefaultCredentials : Credentials,CheckCertificateRevocationList = true
};
using HttpClient httpClient = new(clientHandler)
{
BaseAddress = new UriBuilder()
{
Scheme = BaseUri.Scheme,Host = BaseUri.Host,Port = BaseUri.Port
}.Uri
};
// Process the response stream via XmlReader and CsdlReader.
using var responseStream = httpClient.GetStreamAsync(
new Uri(Path.Combine(BaseUri.AbsolutePath,"$metadata"),UriKind.Relative))
.ConfigureAwait(false).GetAwaiter().GetResult();
using var xmlReader = XmlReader.Create(responseStream);
if (!CsdlReader.TryParse(xmlReader,out var model,out var errors))
{
StringBuilder errorMessages = new();
foreach (var error in errors) errorMessages.Append(error.ErrorMessage).Append("; ");
throw new InvalidOperationException(errorMessages.ToString());
}
// Return and cache the parsed model.
return ParsedModel = model;
}
}
到那时它可以用于以下内容:
Container context = new Container(new Uri("https://targetservice.dev/ODataV4/"),useDefaultCredentials: true);
更新
这似乎是实现中的一个错误,未将凭据信息转发到元数据请求。以下堆栈跟踪显示了 LoadServiceModelFromNetwork
方法的调用:
at Microsoft.OData.Edm.Csdl.CsdlReader.Parse(XmlReader reader)
at Microsoft.OData.Client.DataServiceClientFormat.LoadServiceModelFromNetwork()
at Microsoft.OData.Client.DataServiceClientFormat.get_ServiceModel()
at Microsoft.OData.Client.RequestInfo..ctor(DataServiceContext context)
at Microsoft.OData.Client.DataServiceRequest.CreateExecuteResult(Object source,DataServiceContext context,AsyncCallback callback,Object state,String method)
at Microsoft.OData.Client.DataServiceRequest.Execute[TElement](DataServiceContext context,QueryComponents queryComponents)
at Microsoft.OData.Client.DataServiceQuery`1.Execute()
包中的当前实现如下:
internal IEdmModel LoadServiceModelFromNetwork()
{
DataServiceClientRequestMessage httpRequest;
BuildingRequestEventArgs requestEventArgs = null;
// test hook for injecting a network request to use instead of the default
if (InjectMetadataHttpNetworkRequest != null)
// ...
else
{
// ...
httpRequest = new HttpClientRequestMessage(args);
}
// ...
Task<IODataResponseMessage> asyncResponse =
Task<IODataResponseMessage>.Factory.FromAsync(httpRequest.BeginGetResponse,httpRequest.EndGetResponse,httpRequest);
IODataResponseMessage response = asyncResponse.GetAwaiter().GetResult();
// ...
using (StreamReader streamReader = new StreamReader(response.GetStream()))
using (XmlReader xmlReader = XmlReader.Create(streamReader))
{
return CsdlReader.Parse(xmlReader);
}
}
事实证明,这里的 httpRequest
变量负责处理实际响应。构造函数实现如下:
public HttpClientRequestMessage(DataServiceClientRequestMessageArgs args)
: base(args.ActualMethod)
{
_messageStream = new MemoryStream();
_handler = new HttpClientHandler();
_client = new HttpClient(_handler,disposeHandler: true);
_contentHeaderValueCache = new Dictionary<string,string>();
_effectiveHttpMethod = args.Method;
_requestUrl = args.RequestUri;
_requestMessage = new HttpRequestMessage(new HttpMethod(this.ActualMethod),_requestUrl);
// Now set the headers.
foreach (KeyValuePair<string,string> keyValue in args.Headers)
{
this.SetHeader(keyValue.Key,keyValue.Value);
}
}
凭据和布尔值 UseDefaultCredentials
都不会转发到 HttpClientHandler
。但是 args
提供了此信息。
此外,在构造之后不会设置凭据,并且不会检查响应是否存在无效状态代码,因此最终会出现这种奇怪的行为。
在第一次使用容器之前,从外部设置凭据。毕竟,您不希望每次凭据更改时都修改您的 Container
。您也不想将用于检索这些凭据的机制硬编码到 Container
中。
假设服务器和客户端都在同一个域中,可以使用 Windows 身份验证作为当前用户进行连接,使用 CredentialCache.DefaultNetworkCredentials
var uri=new Uri("https://targetservice.dev/ODataV4/$metadata");
Container context = new Container(uri){
Credentials = CredentialCache.DefaultNetworkCredentials
};
var result = context.People.Execute() as QueryOperationResponse<Person>;
如果您想从非域计算机连接或使用不同的帐户,则必须创建一个 NetworkCredential
实例:
var credential = new NetworkCredential("MyDomain","UserName","Password");
Container context = new Container(uri){
Credentials = credential
};
Windows 身份验证的主要优点是您不需要明确指定凭据。任何远程调用都将使用当前帐户进行。这样就无需存储、更改或泄露密码。这当然假设客户端和服务器都在同一个 Windows 域中,否则服务器将无法识别客户端帐户。
网址错误
异常片段抱怨服务器的响应,而不是身份验证失败。 XmlError : Root element is missing. : (0,0)
表示响应不是一个 XML 文档。这也不是错误响应。如果服务器以 4xx 或 5xx 状态响应,则会引发不同的异常。
服务 URL 错误,不应包含 $metadata
后缀。正如构造函数名称所说的 DataServiceContext(Uri serviceRoot)
,URL 应该是服务的根。该 URL 存储在 BaseUriResolver
属性中:
internal DataServiceContext(Uri serviceRoot,ODataProtocolVersion maxProtocolVersion,ClientEdmModel model)
{
Debug.Assert(model != null,"model != null");
this.model = model;
this.baseUriResolver = UriResolver.CreateFromBaseUri(serviceRoot,ServiceRootParameterName);
元数据 URL 由 GetMetadataUri
方法创建,其代码为:
public virtual Uri GetMetadataUri()
{
// TODO: resolve the location of the metadata endpoint for the service by using an HTTP OPTIONS request
Uri metadataUri = UriUtil.CreateUri(UriUtil.UriToString(
this.BaseUriResolver.GetBaseUriWithSlash()) +
XmlConstants.UriMetadataSegment,UriKind.Absolute);
return metadataUri;
}
使用 https://targetservice.dev/ODataV4/$metadata
作为服务 URL 会导致无效的 https://targetservice.dev/ODataV4/$metadata/$metadata
元数据 URL
为了解决此类错误,可以使用 Fiddler 或其他调试代理来拦截 HTTP 调用并检查发送的内容以及服务器实际返回的内容。
版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。