OWIN Cookie 身份验证 - 使用 Kerberos 委派模拟 SQL 服务器
OWIN Cookie Authentication - Impersonation to SQL Server with Kerberos Delegation
在对 Identity 2.0、模拟、委派和 Kerberos 进行了数周的研究之后,我仍然无法找到一种解决方案来模拟我在 MVC 应用程序中使用 OWIN 创建的 ClaimsIdentity 用户。具体到我的场景如下。
Windows 身份验证已禁用 + 匿名已启用。
我正在使用 OWIN 启动程序 class 根据我们的 Active Directory 手动验证用户。然后我将一些属性打包到一个 cookie 中,该 cookie 在整个应用程序的其余部分都可用。 This 是我在设置这些 class 时引用的 link。
Startup.Auth.cs
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = MyAuthentication.ApplicationCookie,
LoginPath = new PathString("/Login"),
Provider = new CookieAuthenticationProvider(),
CookieName = "SessionName",
CookieHttpOnly = true,
ExpireTimeSpan = TimeSpan.FromHours(double.Parse(ConfigurationManager.AppSettings["CookieLength"]))
});
AuthenticationService.cs
using System;
using System.DirectoryServices.AccountManagement;
using System.DirectoryServices;
using System.Security.Claims;
using Microsoft.Owin.Security;
using System.Configuration;
using System.Collections.Generic;
using System.Linq;
namespace mine.Security
{
public class AuthenticationService
{
private readonly IAuthenticationManager _authenticationManager;
private PrincipalContext _context;
private UserPrincipal _userPrincipal;
private ClaimsIdentity _identity;
public AuthenticationService(IAuthenticationManager authenticationManager)
{
_authenticationManager = authenticationManager;
}
/// <summary>
/// Check if username and password matches existing account in AD.
/// </summary>
/// <param name="username"></param>
/// <param name="password"></param>
/// <returns></returns>
public AuthenticationResult SignIn(String username, String password)
{
// connect to active directory
_context = new PrincipalContext(ContextType.Domain,
ConfigurationManager.ConnectionStrings["LdapServer"].ConnectionString,
ConfigurationManager.ConnectionStrings["LdapContainer"].ConnectionString,
ContextOptions.SimpleBind,
ConfigurationManager.ConnectionStrings["LDAPUser"].ConnectionString,
ConfigurationManager.ConnectionStrings["LDAPPass"].ConnectionString);
// try to find if the user exists
_userPrincipal = UserPrincipal.FindByIdentity(_context, IdentityType.SamAccountName, username);
if (_userPrincipal == null)
{
return new AuthenticationResult("There was an issue authenticating you.");
}
// try to validate credentials
if (!_context.ValidateCredentials(username, password))
{
return new AuthenticationResult("Incorrect username/password combination.");
}
// ensure account is not locked out
if (_userPrincipal.IsAccountLockedOut())
{
return new AuthenticationResult("There was an issue authenticating you.");
}
// ensure account is enabled
if (_userPrincipal.Enabled.HasValue && _userPrincipal.Enabled.Value == false)
{
return new AuthenticationResult("There was an issue authenticating you.");
}
MyContext dbcontext = new MyContext();
var appUser = dbcontext.AppUsers.Where(a => a.ActiveDirectoryLogin.ToLower() == "domain\" +_userPrincipal.SamAccountName.ToLower()).FirstOrDefault();
if (appUser == null)
{
return new AuthenticationResult("Sorry, you have not been granted user access to the MED application.");
}
// pass both adprincipal and appuser model to build claims identity
_identity = CreateIdentity(_userPrincipal, appUser);
_authenticationManager.SignOut(MyAuthentication.ApplicationCookie);
_authenticationManager.SignIn(new AuthenticationProperties() { IsPersistent = false }, _identity);
return new AuthenticationResult();
}
/// <summary>
/// Creates identity and packages into cookie
/// </summary>
/// <param name="userPrincipal"></param>
/// <returns></returns>
private ClaimsIdentity CreateIdentity(UserPrincipal userPrincipal, AppUser appUser)
{
var identity = new ClaimsIdentity(MyAuthentication.ApplicationCookie, ClaimsIdentity.DefaultNameClaimType, ClaimsIdentity.DefaultRoleClaimType);
identity.AddClaim(new Claim("http://schemas.microsoft.com/accesscontrolservice/2010/07/claims/identityprovider", "Active Directory"));
identity.AddClaim(new Claim(ClaimTypes.GivenName, userPrincipal.GivenName));
identity.AddClaim(new Claim(ClaimTypes.Surname, userPrincipal.Surname));
identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, userPrincipal.SamAccountName));
identity.AddClaim(new Claim(ClaimTypes.Name, userPrincipal.SamAccountName));
identity.AddClaim(new Claim(ClaimTypes.Upn, userPrincipal.UserPrincipalName));
if (!String.IsNullOrEmpty(userPrincipal.EmailAddress))
{
identity.AddClaim(new Claim(ClaimTypes.Email, userPrincipal.EmailAddress));
}
// db claims
if (appUser.DefaultAppOfficeId != null)
{
identity.AddClaim(new Claim("DefaultOffice", appUser.AppOffice.OfficeName));
}
if (appUser.CurrentAppOfficeId != null)
{
identity.AddClaim(new Claim("Office", appUser.AppOffice1.OfficeName));
}
var claims = new List<Claim>();
DirectoryEntry dirEntry = (DirectoryEntry)userPrincipal.GetUnderlyingObject();
foreach (string groupDn in dirEntry.Properties["memberOf"])
{
string[] parts = groupDn.Replace("CN=", "").Split(',');
claims.Add(new Claim(ClaimTypes.Role, parts[0]));
}
if (claims.Count > 0)
{
identity.AddClaims(claims);
}
return identity;
}
/// <summary>
/// Authentication result class
/// </summary>
public class AuthenticationResult
{
public AuthenticationResult(string errorMessage = null)
{
ErrorMessage = errorMessage;
}
public String ErrorMessage { get; private set; }
public Boolean IsSuccess => String.IsNullOrEmpty(ErrorMessage);
}
}
}
那部分似乎工作得很好。但是,我需要能够在调用数据库时模拟 ClaimsIdentity,因为数据库具有角色级别的安全设置。我需要在该用户会话的剩余部分的 ClaimsIdentity 上下文中完成连接。
- 我已经为 Kerberos 设置了 SPN,我知道它可以工作。这个应用程序是
以前 windows 使用 Kerberos 委托进行身份验证并且它工作正常。
- 应用程序池 运行 在 SPN 中使用的服务帐户下,该帐户具有委派权限。
- 我创建的 Identity 对象几乎只在应用程序上下文中使用。我的意思是我主要从 Active Directory 中获取所有必要的属性,但是将从数据库中创建两个属性。此标识未直接映射到 sql table 或任何其他数据源。
谁能帮我举个例子,在对 SQL 服务器数据库进行数据库查询时,我可以模拟 ClaimsIdentity 对象?
也许我误解了这个问题但是:
要使用 Windows 身份验证建立 SQL 服务器连接,连接字符串必须使用 'Integrated Security',这意味着它将使用建立连接的当前安全上下文.通常这将是您的 AppPool 用户,在您的情况下是服务帐户。据我所知,you can't propagate your impersonation to the AppPool thread automatically using Kerberos auth。这是我找到的引述:
In IIS, only Basic Authentication logs users on with a security token
that flows across the network to a remote SQL server. By default,
other IIS security modes used in conjunction with the identity
configuration element settings will not result in a token that can
authenticate to a remote SQL Server.
因此,如果您想冒充其他用户,则必须在您正在冒充的用户的主体下启动一个新线程。这样,集成安全连接将使用该用户的 Windows 身份验证连接到 SQL 服务器。
我不确定具体该怎么做,但这里有一些东西可能会把你推向正确的方向:
public void NewThreadToRunSQLQueries(object claimsIdentity) {
if (claimsIdentity as ClaimsIdentity == null) {
throw new ArgumentNullException("claimsIdentity");
}
ClaimsIdentity claimsIdentity = (ClaimsIdentity)claimsIdentity;
var claimsIdentitylst = new ClaimsIdentityCollection(new List<IClaimsIdentity> { claimsIdentity });
IClaimsPrincipal claimsPrincipal = new ClaimsPrincipal(claimsIdentitylst);
Thread.CurrentPrincipal = claimsPrincipal; //Set current thread principal
using(SqlConnection connection = new SqlConnection("Server=myServerAddress;Database=myDataBase;Integrated Security=True;"))
{
connection.Open(); //Open connection under impersonated user account
//Run SQL Queries
}
}
Thread thread = new Thread(NewThreadToRunSQLQueries);
thread.Start(_identity);
编辑:
关于您对如何构建此结构的评论 "global",假设您可以访问身份验证处理程序中的 HttpContext,您可以这样做:
var principal = new ClaimsPrincipal(_identity);
Thread.CurrentPrincipal = principal;
if (HttpContext.Current != null)
{
HttpContext.Current.User = principal;
}
因此理论上 IIS 的工作线程现在应该 运行 在经过身份验证的用户(模拟)下。与 SQL 服务器的可信连接应该是可能的。我说理论上是因为我自己还没有尝试过。但最坏的情况是你可以从 HttpContext 中获取声明来启动一个单独的线程,就像我上面的例子一样。但如果这本身有效,你甚至不必像我最初提到的那样开始一个新线程。
我猜您缺少 IIS 中的配置点,您需要允许 IIS 将用户上下文传递给您,这不是默认设置。
在尝试 "fix" 代码之前先看看 this document。如果这没有帮助让我们知道并告诉我们您的设置,单靠代码可能无法解决问题。
[已解决更新 2-1-19] 我已经写了一篇博客 post 详细介绍了这个过程并且可以使用 here.
我能够通过执行以下操作来完成此操作。我创建了一个 class 来使这些方法可重用。在那个 class 中,我使用 System.IdentityModel.Selectors
和 System.IdentityModel.Tokens
库生成一个 KeberosReceiverSecurityToken
并将其存储在内存中。
public class KerberosTokenCacher
{
public KerberosTokenCacher()
{
}
public KerberosReceiverSecurityToken WriteToCache(string contextUsername, string contextPassword)
{
KerberosSecurityTokenProvider provider =
new KerberosSecurityTokenProvider("YOURSPN",
TokenImpersonationLevel.Impersonation,
new NetworkCredential(contextUsername.ToLower(), contextPassword, "yourdomain"));
KerberosRequestorSecurityToken requestorToken = provider.GetToken(TimeSpan.FromMinutes(double.Parse(ConfigurationManager.AppSettings["KerberosTokenExpiration"]))) as KerberosRequestorSecurityToken;
KerberosReceiverSecurityToken receiverToken = new KerberosReceiverSecurityToken(requestorToken.GetRequest());
IAppCache appCache = new CachingService();
KerberosReceiverSecurityToken tokenFactory() => receiverToken;
return appCache.GetOrAdd(contextUsername.ToLower(), tokenFactory); // this will either add the token or get the token if it exists
}
public KerberosReceiverSecurityToken ReadFromCache(string contextUsername)
{
IAppCache appCache = new CachingService();
KerberosReceiverSecurityToken token = appCache.Get<KerberosReceiverSecurityToken>(contextUsername.ToLower());
return token;
}
public void DeleteFromCache(string contextUsername)
{
IAppCache appCache = new CachingService();
KerberosReceiverSecurityToken token = appCache.Get<KerberosReceiverSecurityToken>(contextUsername.ToLower());
if(token != null)
{
appCache.Remove(contextUsername.ToLower());
}
}
}
现在,当用户使用我的 AuthenticationService 登录时,我会创建票证并将其存储在内存中。当他们注销时,我做相反的事情并从缓存中删除票证。最后一部分(我仍在寻找更好的方法来完成此操作),我向 dbcontext class.
的构造函数添加了一些代码
public MyContext(bool impersonate = true): base("name=MyContext")
{
if (impersonate)
{
var currentUsername = HttpContext.Current.GetOwinContext().Authentication.User?.Identity?.Name;
if (!string.IsNullOrEmpty(currentUsername)){
KerberosTokenCacher kerberosTokenCacher = new KerberosTokenCacher();
KerberosReceiverSecurityToken token = kerberosTokenCacher.ReadFromCache(currentUsername);
if (token != null)
{
token.WindowsIdentity.Impersonate();
}
else
{
// token has expired or cache has expired so you must log in again
HttpContext.Current.Response.Redirect("Login/Logoff");
}
}
}
}
显然它绝对不完美,但它允许我对活动目录使用 Owin Cookie 身份验证并生成 Kerberos 票证允许连接到 SQL 数据库的用户上下文认证。
在对 Identity 2.0、模拟、委派和 Kerberos 进行了数周的研究之后,我仍然无法找到一种解决方案来模拟我在 MVC 应用程序中使用 OWIN 创建的 ClaimsIdentity 用户。具体到我的场景如下。
Windows 身份验证已禁用 + 匿名已启用。
我正在使用 OWIN 启动程序 class 根据我们的 Active Directory 手动验证用户。然后我将一些属性打包到一个 cookie 中,该 cookie 在整个应用程序的其余部分都可用。 This 是我在设置这些 class 时引用的 link。
Startup.Auth.cs
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = MyAuthentication.ApplicationCookie,
LoginPath = new PathString("/Login"),
Provider = new CookieAuthenticationProvider(),
CookieName = "SessionName",
CookieHttpOnly = true,
ExpireTimeSpan = TimeSpan.FromHours(double.Parse(ConfigurationManager.AppSettings["CookieLength"]))
});
AuthenticationService.cs
using System;
using System.DirectoryServices.AccountManagement;
using System.DirectoryServices;
using System.Security.Claims;
using Microsoft.Owin.Security;
using System.Configuration;
using System.Collections.Generic;
using System.Linq;
namespace mine.Security
{
public class AuthenticationService
{
private readonly IAuthenticationManager _authenticationManager;
private PrincipalContext _context;
private UserPrincipal _userPrincipal;
private ClaimsIdentity _identity;
public AuthenticationService(IAuthenticationManager authenticationManager)
{
_authenticationManager = authenticationManager;
}
/// <summary>
/// Check if username and password matches existing account in AD.
/// </summary>
/// <param name="username"></param>
/// <param name="password"></param>
/// <returns></returns>
public AuthenticationResult SignIn(String username, String password)
{
// connect to active directory
_context = new PrincipalContext(ContextType.Domain,
ConfigurationManager.ConnectionStrings["LdapServer"].ConnectionString,
ConfigurationManager.ConnectionStrings["LdapContainer"].ConnectionString,
ContextOptions.SimpleBind,
ConfigurationManager.ConnectionStrings["LDAPUser"].ConnectionString,
ConfigurationManager.ConnectionStrings["LDAPPass"].ConnectionString);
// try to find if the user exists
_userPrincipal = UserPrincipal.FindByIdentity(_context, IdentityType.SamAccountName, username);
if (_userPrincipal == null)
{
return new AuthenticationResult("There was an issue authenticating you.");
}
// try to validate credentials
if (!_context.ValidateCredentials(username, password))
{
return new AuthenticationResult("Incorrect username/password combination.");
}
// ensure account is not locked out
if (_userPrincipal.IsAccountLockedOut())
{
return new AuthenticationResult("There was an issue authenticating you.");
}
// ensure account is enabled
if (_userPrincipal.Enabled.HasValue && _userPrincipal.Enabled.Value == false)
{
return new AuthenticationResult("There was an issue authenticating you.");
}
MyContext dbcontext = new MyContext();
var appUser = dbcontext.AppUsers.Where(a => a.ActiveDirectoryLogin.ToLower() == "domain\" +_userPrincipal.SamAccountName.ToLower()).FirstOrDefault();
if (appUser == null)
{
return new AuthenticationResult("Sorry, you have not been granted user access to the MED application.");
}
// pass both adprincipal and appuser model to build claims identity
_identity = CreateIdentity(_userPrincipal, appUser);
_authenticationManager.SignOut(MyAuthentication.ApplicationCookie);
_authenticationManager.SignIn(new AuthenticationProperties() { IsPersistent = false }, _identity);
return new AuthenticationResult();
}
/// <summary>
/// Creates identity and packages into cookie
/// </summary>
/// <param name="userPrincipal"></param>
/// <returns></returns>
private ClaimsIdentity CreateIdentity(UserPrincipal userPrincipal, AppUser appUser)
{
var identity = new ClaimsIdentity(MyAuthentication.ApplicationCookie, ClaimsIdentity.DefaultNameClaimType, ClaimsIdentity.DefaultRoleClaimType);
identity.AddClaim(new Claim("http://schemas.microsoft.com/accesscontrolservice/2010/07/claims/identityprovider", "Active Directory"));
identity.AddClaim(new Claim(ClaimTypes.GivenName, userPrincipal.GivenName));
identity.AddClaim(new Claim(ClaimTypes.Surname, userPrincipal.Surname));
identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, userPrincipal.SamAccountName));
identity.AddClaim(new Claim(ClaimTypes.Name, userPrincipal.SamAccountName));
identity.AddClaim(new Claim(ClaimTypes.Upn, userPrincipal.UserPrincipalName));
if (!String.IsNullOrEmpty(userPrincipal.EmailAddress))
{
identity.AddClaim(new Claim(ClaimTypes.Email, userPrincipal.EmailAddress));
}
// db claims
if (appUser.DefaultAppOfficeId != null)
{
identity.AddClaim(new Claim("DefaultOffice", appUser.AppOffice.OfficeName));
}
if (appUser.CurrentAppOfficeId != null)
{
identity.AddClaim(new Claim("Office", appUser.AppOffice1.OfficeName));
}
var claims = new List<Claim>();
DirectoryEntry dirEntry = (DirectoryEntry)userPrincipal.GetUnderlyingObject();
foreach (string groupDn in dirEntry.Properties["memberOf"])
{
string[] parts = groupDn.Replace("CN=", "").Split(',');
claims.Add(new Claim(ClaimTypes.Role, parts[0]));
}
if (claims.Count > 0)
{
identity.AddClaims(claims);
}
return identity;
}
/// <summary>
/// Authentication result class
/// </summary>
public class AuthenticationResult
{
public AuthenticationResult(string errorMessage = null)
{
ErrorMessage = errorMessage;
}
public String ErrorMessage { get; private set; }
public Boolean IsSuccess => String.IsNullOrEmpty(ErrorMessage);
}
}
}
那部分似乎工作得很好。但是,我需要能够在调用数据库时模拟 ClaimsIdentity,因为数据库具有角色级别的安全设置。我需要在该用户会话的剩余部分的 ClaimsIdentity 上下文中完成连接。
- 我已经为 Kerberos 设置了 SPN,我知道它可以工作。这个应用程序是 以前 windows 使用 Kerberos 委托进行身份验证并且它工作正常。
- 应用程序池 运行 在 SPN 中使用的服务帐户下,该帐户具有委派权限。
- 我创建的 Identity 对象几乎只在应用程序上下文中使用。我的意思是我主要从 Active Directory 中获取所有必要的属性,但是将从数据库中创建两个属性。此标识未直接映射到 sql table 或任何其他数据源。
谁能帮我举个例子,在对 SQL 服务器数据库进行数据库查询时,我可以模拟 ClaimsIdentity 对象?
也许我误解了这个问题但是:
要使用 Windows 身份验证建立 SQL 服务器连接,连接字符串必须使用 'Integrated Security',这意味着它将使用建立连接的当前安全上下文.通常这将是您的 AppPool 用户,在您的情况下是服务帐户。据我所知,you can't propagate your impersonation to the AppPool thread automatically using Kerberos auth。这是我找到的引述:
In IIS, only Basic Authentication logs users on with a security token that flows across the network to a remote SQL server. By default, other IIS security modes used in conjunction with the identity configuration element settings will not result in a token that can authenticate to a remote SQL Server.
因此,如果您想冒充其他用户,则必须在您正在冒充的用户的主体下启动一个新线程。这样,集成安全连接将使用该用户的 Windows 身份验证连接到 SQL 服务器。
我不确定具体该怎么做,但这里有一些东西可能会把你推向正确的方向:
public void NewThreadToRunSQLQueries(object claimsIdentity) {
if (claimsIdentity as ClaimsIdentity == null) {
throw new ArgumentNullException("claimsIdentity");
}
ClaimsIdentity claimsIdentity = (ClaimsIdentity)claimsIdentity;
var claimsIdentitylst = new ClaimsIdentityCollection(new List<IClaimsIdentity> { claimsIdentity });
IClaimsPrincipal claimsPrincipal = new ClaimsPrincipal(claimsIdentitylst);
Thread.CurrentPrincipal = claimsPrincipal; //Set current thread principal
using(SqlConnection connection = new SqlConnection("Server=myServerAddress;Database=myDataBase;Integrated Security=True;"))
{
connection.Open(); //Open connection under impersonated user account
//Run SQL Queries
}
}
Thread thread = new Thread(NewThreadToRunSQLQueries);
thread.Start(_identity);
编辑:
关于您对如何构建此结构的评论 "global",假设您可以访问身份验证处理程序中的 HttpContext,您可以这样做:
var principal = new ClaimsPrincipal(_identity);
Thread.CurrentPrincipal = principal;
if (HttpContext.Current != null)
{
HttpContext.Current.User = principal;
}
因此理论上 IIS 的工作线程现在应该 运行 在经过身份验证的用户(模拟)下。与 SQL 服务器的可信连接应该是可能的。我说理论上是因为我自己还没有尝试过。但最坏的情况是你可以从 HttpContext 中获取声明来启动一个单独的线程,就像我上面的例子一样。但如果这本身有效,你甚至不必像我最初提到的那样开始一个新线程。
我猜您缺少 IIS 中的配置点,您需要允许 IIS 将用户上下文传递给您,这不是默认设置。
在尝试 "fix" 代码之前先看看 this document。如果这没有帮助让我们知道并告诉我们您的设置,单靠代码可能无法解决问题。
[已解决更新 2-1-19] 我已经写了一篇博客 post 详细介绍了这个过程并且可以使用 here.
我能够通过执行以下操作来完成此操作。我创建了一个 class 来使这些方法可重用。在那个 class 中,我使用 System.IdentityModel.Selectors
和 System.IdentityModel.Tokens
库生成一个 KeberosReceiverSecurityToken
并将其存储在内存中。
public class KerberosTokenCacher
{
public KerberosTokenCacher()
{
}
public KerberosReceiverSecurityToken WriteToCache(string contextUsername, string contextPassword)
{
KerberosSecurityTokenProvider provider =
new KerberosSecurityTokenProvider("YOURSPN",
TokenImpersonationLevel.Impersonation,
new NetworkCredential(contextUsername.ToLower(), contextPassword, "yourdomain"));
KerberosRequestorSecurityToken requestorToken = provider.GetToken(TimeSpan.FromMinutes(double.Parse(ConfigurationManager.AppSettings["KerberosTokenExpiration"]))) as KerberosRequestorSecurityToken;
KerberosReceiverSecurityToken receiverToken = new KerberosReceiverSecurityToken(requestorToken.GetRequest());
IAppCache appCache = new CachingService();
KerberosReceiverSecurityToken tokenFactory() => receiverToken;
return appCache.GetOrAdd(contextUsername.ToLower(), tokenFactory); // this will either add the token or get the token if it exists
}
public KerberosReceiverSecurityToken ReadFromCache(string contextUsername)
{
IAppCache appCache = new CachingService();
KerberosReceiverSecurityToken token = appCache.Get<KerberosReceiverSecurityToken>(contextUsername.ToLower());
return token;
}
public void DeleteFromCache(string contextUsername)
{
IAppCache appCache = new CachingService();
KerberosReceiverSecurityToken token = appCache.Get<KerberosReceiverSecurityToken>(contextUsername.ToLower());
if(token != null)
{
appCache.Remove(contextUsername.ToLower());
}
}
}
现在,当用户使用我的 AuthenticationService 登录时,我会创建票证并将其存储在内存中。当他们注销时,我做相反的事情并从缓存中删除票证。最后一部分(我仍在寻找更好的方法来完成此操作),我向 dbcontext class.
的构造函数添加了一些代码public MyContext(bool impersonate = true): base("name=MyContext")
{
if (impersonate)
{
var currentUsername = HttpContext.Current.GetOwinContext().Authentication.User?.Identity?.Name;
if (!string.IsNullOrEmpty(currentUsername)){
KerberosTokenCacher kerberosTokenCacher = new KerberosTokenCacher();
KerberosReceiverSecurityToken token = kerberosTokenCacher.ReadFromCache(currentUsername);
if (token != null)
{
token.WindowsIdentity.Impersonate();
}
else
{
// token has expired or cache has expired so you must log in again
HttpContext.Current.Response.Redirect("Login/Logoff");
}
}
}
}
显然它绝对不完美,但它允许我对活动目录使用 Owin Cookie 身份验证并生成 Kerberos 票证允许连接到 SQL 数据库的用户上下文认证。