所以,代码中有一点 PEBKAC。尽管它使用了builder.Authentication = SqlAuthenticationMethod.ActiveDirectoryInteractive;,但该类实际上是在尝试使用ActiveDirectoryIntegrated。所以我的AD课从未真正被击中。此外,在示例代码中它实际上也不会起作用,因为 ActiveDirectoryIntegrated 的 case 语句存在 - 我已经在我的本地副本中删除了它。
我实际上需要使用正确的ActiveDirectoryInteractive 代码来连接它。一旦我这样做了,它就能够对系统进行一次身份验证。这使得所有数据库连接都可以正常工作,而无需额外的浏览器检查。
设置
static void Main(string[] args)
{
var provider = new ActiveDirectoryAuthProvider();
SqlAuthenticationProvider.SetProvider(
SqlAuthenticationMethod.ActiveDirectoryInteractive,
//SC.SqlAuthenticationMethod.ActiveDirectoryIntegrated, // Alternatives.
//SC.SqlAuthenticationMethod.ActiveDirectoryPassword,
provider);
}
ActiveDirectoryAuthProvider
public class ActiveDirectoryAuthProvider : SqlAuthenticationProvider
{
private readonly string _clientId = "MyClientID";
private Uri _redirectURL { get; set; } = new Uri("http://localhost:8089");
private AD.AuthenticationContext AuthContext { get; set; }
private TokenCache Cache { get; set; }
public ActiveDirectoryAuthProvider()
{
Cache = new TokenCache();
}
public override async TT.Task<SC.SqlAuthenticationToken> AcquireTokenAsync(SC.SqlAuthenticationParameters parameters)
{
var authContext = AuthContext ?? new AD.AuthenticationContext(parameters.Authority, Cache);
authContext.CorrelationId = parameters.ConnectionId;
AD.AuthenticationResult result;
try
{
result = await authContext.AcquireTokenSilentAsync(
parameters.Resource,
_clientId);
}
catch (AdalSilentTokenAcquisitionException)
{
result = await authContext.AcquireTokenAsync(
parameters.Resource,
_clientId,
_redirectURL,
new AD.PlatformParameters(PromptBehavior.Auto, new CustomWebUi()),
new UserIdentifier(parameters.UserId, UserIdentifierType.RequiredDisplayableId));
}
var token = new SC.SqlAuthenticationToken(result.AccessToken, result.ExpiresOn);
return token;
}
public override bool IsSupported(SC.SqlAuthenticationMethod authenticationMethod)
{
return authenticationMethod == SC.SqlAuthenticationMethod.ActiveDirectoryInteractive;
}
}
这里有一些不同之处:
- 我添加了一个内存令牌缓存
- 我已将 AuthContext 移动到类的属性中,以便在运行之间将其保留在内存中
- 我设置了
_redirectURL 属性 = http://localhost:8089
- 在还原之前,我添加了对令牌的静默检查
最后,我创建了自己的 ICustomWebUi 实现,用于处理加载浏览器登录和响应:
CustomWebUi
internal class CustomWebUi : ICustomWebUi
{
public async Task<Uri> AcquireAuthorizationCodeAsync(Uri authorizationUri, Uri redirectUri)
{
using (var listener = new SingleMessageTcpListener(redirectUri.Port))
{
Uri authCode = null;
var listenerTask = listener.ListenToSingleRequestAndRespondAsync(u => {
authCode = u;
return @"
<html>
<body>
<p>Successfully Authenticated, you may now close this window</p>
</body>
</html>";
}, System.Threading.CancellationToken.None);
var ps = new ProcessStartInfo(authorizationUri.ToString())
{
UseShellExecute = true,
Verb = "open"
};
Process.Start(ps);
await listenerTask;
return authCode;
}
}
}
因为我已将重定向设置回 localhost,并且此代码位于控制台应用程序中,所以我需要在端口上侦听响应并在应用程序中捕获它,然后向浏览器显示一个值以指示它一切正常。
为了监听端口,我使用了一个从MS Github 抄袭的监听器类:
SingleMessageTcpListener
/// <summary>
/// This object is responsible for listening to a single TCP request, on localhost:port,
/// extracting the uri, parsing
/// </summary>
/// <remarks>
/// The underlying TCP listener might capture multiple requests, but only the first one is handled.
///
/// Cribbed this class from https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/blob/9e0f57b53edfdcf027cbff401d3ca6c02e95ef1b/tests/devapps/NetCoreTestApp/Experimental/SingleMessageTcpListener.cs
/// </remarks>
internal class SingleMessageTcpListener : IDisposable
{
private readonly int _port;
private readonly System.Net.Sockets.TcpListener _tcpListener;
public SingleMessageTcpListener(int port)
{
if (port < 1 || port == 80)
{
throw new ArgumentOutOfRangeException("Expected a valid port number, > 0, not 80");
}
_port = port;
_tcpListener = new System.Net.Sockets.TcpListener(IPAddress.Loopback, _port);
}
public async Task ListenToSingleRequestAndRespondAsync(
Func<Uri, string> responseProducer,
CancellationToken cancellationToken)
{
cancellationToken.Register(() => _tcpListener.Stop());
_tcpListener.Start();
TcpClient tcpClient = null;
try
{
tcpClient =
await AcceptTcpClientAsync(cancellationToken)
.ConfigureAwait(false);
await ExtractUriAndRespondAsync(tcpClient, responseProducer, cancellationToken).ConfigureAwait(false);
}
finally
{
tcpClient?.Close();
}
}
/// <summary>
/// AcceptTcpClientAsync does not natively support cancellation, so use this wrapper. Make sure
/// the cancellation token is registered to stop the listener.
/// </summary>
/// <remarks>See https://stackoverflow.com/questions/19220957/tcplistener-how-to-stop-listening-while-awaiting-accepttcpclientasync</remarks>
private async Task<TcpClient> AcceptTcpClientAsync(CancellationToken token)
{
try
{
return await _tcpListener.AcceptTcpClientAsync().ConfigureAwait(false);
}
catch (Exception ex) when (token.IsCancellationRequested)
{
throw new OperationCanceledException("Cancellation was requested while awaiting TCP client connection.", ex);
}
}
private async Task ExtractUriAndRespondAsync(
TcpClient tcpClient,
Func<Uri, string> responseProducer,
CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
string httpRequest = await GetTcpResponseAsync(tcpClient, cancellationToken).ConfigureAwait(false);
Uri uri = ExtractUriFromHttpRequest(httpRequest);
// write an "OK, please close the browser message"
await WriteResponseAsync(responseProducer(uri), tcpClient.GetStream(), cancellationToken)
.ConfigureAwait(false);
}
private Uri ExtractUriFromHttpRequest(string httpRequest)
{
string regexp = @"GET \/\?(.*) HTTP";
string getQuery = null;
Regex r1 = new Regex(regexp);
Match match = r1.Match(httpRequest);
if (!match.Success)
{
throw new InvalidOperationException("Not a GET query");
}
getQuery = match.Groups[1].Value;
UriBuilder uriBuilder = new UriBuilder();
uriBuilder.Query = getQuery;
uriBuilder.Port = _port;
return uriBuilder.Uri;
}
private static async Task<string> GetTcpResponseAsync(TcpClient client, CancellationToken cancellationToken)
{
NetworkStream networkStream = client.GetStream();
byte[] readBuffer = new byte[1024];
StringBuilder stringBuilder = new StringBuilder();
int numberOfBytesRead = 0;
// Incoming message may be larger than the buffer size.
do
{
numberOfBytesRead = await networkStream.ReadAsync(readBuffer, 0, readBuffer.Length, cancellationToken)
.ConfigureAwait(false);
string s = Encoding.ASCII.GetString(readBuffer, 0, numberOfBytesRead);
stringBuilder.Append(s);
}
while (networkStream.DataAvailable);
return stringBuilder.ToString();
}
private async Task WriteResponseAsync(
string message,
NetworkStream stream,
CancellationToken cancellationToken)
{
string fullResponse = $"HTTP/1.1 200 OK\r\n\r\n{message}";
var response = Encoding.ASCII.GetBytes(fullResponse);
await stream.WriteAsync(response, 0, response.Length, cancellationToken).ConfigureAwait(false);
await stream.FlushAsync(cancellationToken).ConfigureAwait(false);
}
public void Dispose()
{
_tcpListener?.Stop();
}
}
所有这些都准备就绪后,浏览器会在连接到资源上的第一个数据库时打开,并且在连接之间重复使用令牌。