Thinktecture Identity Server - 配置、定制






4.91/5 (22投票s)
安装、扩展 Identity Server 并实现会话令牌缓存
引言
在我最近的一个项目中,我们希望使用 Thinktecture Identity Server 来满足我们的身份验证/授权需求。在这个过程中,我遇到了许多与配置相关的问题和发现。我只想分享我的发现,希望对其他开发者有所帮助。
上图展示了我想要实现的目标,我认为这就像一个典型的 Web 应用程序。
我想要实现的目标
- 安装和配置 Identity Server
- 扩展 Identity Server 以使用我们自己的数据存储来检查用户凭据并获取用户声明
- 实现 SecurityToken 缓存
- 将令牌传递给我们的 REST API 服务
在阅读本文之前,您绝对应该阅读许多文章。我已将所有参考资料放在文章底部。我只会讨论要点和每个要点的解决方案。
Identity Server 安装
我能够顺利安装 Identity Server。有许多有用的资源可用,您应该不会遇到任何问题。在使用自签名证书时,我遇到了一些小问题,因为它们在从其他机器使用时不受信任。因此,我想使用 OpenSSL,以便我可以设置一个真实的证书颁发机构并按需颁发证书。
我想要实现的另一件事是让 Identity Server 使用我们自己的数据库来检查用户凭据,而不是使用其自己的数据存储。
证书
Identity Server 需要至少一个 SSL 证书才能运行,因为它需要托管在 HTTPS 上。它还需要另外 2 个证书来签名安全令牌和加密,但您可以为所有 3 个需求使用同一个证书。因此,现在一个证书应该就足够了。
REM Create CA root certificate
openssl req -x509 -nodes -days 3650 -subj "/C=US/L=Redmond/O=XYZ/OU=Technology/CN=XYZ Inc"
-newkey rsa:2048 -keyout xyzCA.key -out xyzCA.crt -config openssl.conf
openssl pkcs12 -export -out xyzCA.pfx -inkey xyzCA.key -in xyzCA.crt
在所有 3 台服务器(STS 服务器、UI 服务器、API 服务器)上安装根证书。如果它们都在一台服务器上,则只需安装一次。
@echo off
rem set server="sts.xyz.com"
set /p server="Enter Server Name: " %=%
REM Create SSL certificate for IIS, which trusts the root certificate
openssl req -nodes -days 3650
-subj "/C=US/L=Redmond/O=XYZ/OU=Technology/CN=%server%"
-newkey rsa:2048 -keyout %server%.key -out %server%.csr -config openssl.conf
openssl x509 -req -days 3650 -in %server%.csr -CA xyzCA.crt
-CAkey xyzCA.key -CAcreateserial -out %server%.crt
openssl pkcs12 -export -out %server%.pfx -inkey %server%.key
-in %server%.crt -name "Server Certificate - %server%"
pause
获取证书后,请按照 Vimeo 上的 Identity Server 设置视频 http://vimeo.com/51088126 操作,应该会很直接。
以下是一些您需要创建的配置的屏幕截图
扩展 Identity Server
我们希望使用自己的数据库来存储用户详细信息,如声明等,因为它已与现有应用程序集成。
您可以按照 此链接,其中提供了分步说明。
处理复杂声明
通常,声明存储为简单的键/值对,两者都为“string
”类型,以保持简单并减少依赖性。但是,我们想在声明旁边存储一些额外的信息(如对象)。我最初只是将复杂对象序列化为 JSON 字符串,并将该值作为声明值存储,然后我能够在接收端使用 JSON.NET 反序列化它。虽然这可行,但我发现了一个很好的文章,其中找到了一个更优雅的方法。您可以在 此处 阅读。
UI Web 应用程序 - 配置
在 UI 端,我们不想将用户重定向到 Identity Server 站点(这是您通常看到的)。相反,我们希望拥有自己的登录屏幕,就像常规的表单身份验证站点一样,并在后台使用 STS 来检查用户凭据并获取关联的声明。
web.config 条目如下
<system.identitymodel>
<identityconfiguration savebootstrapcontext="true">
<audienceuris>
<add value="http://www.xyz.com/yourapp">
</audienceuris>
<securitytokenhandlers>
<add type="System.IdentityModel.Tokens.JwtSecurityTokenHandler,
System.IdentityModel.Tokens.Jwt, Version=2.0.0.0, Culture=neutral,
PublicKeyToken=31bf3856ad364e35">
<securitytokenhandlerconfiguration>
<issuertokenresolver type="System.IdentityModel.Tokens.X509CertificateStoreTokenResolver,
System.IdentityModel, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
<certificatevalidation certificatevalidationmode="PeerOrChainTrust"
trustedstorelocation="LocalMachine" revocationmode="NoCheck">
<issuernameregistry type="System.IdentityModel.Tokens.ValidatingIssuerNameRegistry,
System.IdentityModel.Tokens.ValidatingIssuerNameRegistry,
Version=2.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35">
<authority name="http://identityserver.v2.xyz.com/trust">
<keys>
<add thumbprint="C2B2219F3CAC53658E796C0402360D90AEFA08FC">
</keys>
<validissuers>
<add name="http://identityserver.v2.xyz.com/trust">
</validissuers>
</authority>
</issuernameregistry>
</securitytokenhandlerconfiguration>
</securitytokenhandlers>
<claimsauthorizationmanager
type="IdentityServer.Demo.Common.Security.CustomAuthorizationManager,
IdentityServer.Demo.Common">
</identityconfiguration>
</system.identitymodel>
<system.identitymodel.services>
<federationconfiguration>
<cookiehandler mode="Default" requiressl="false">
<wsfederation realm="http://www.xyz.com/yourapp"
issuer="https://sts.xyz.com/issue/wsfed"
passiveredirectenabled="false" requirehttps="true">
</federationconfiguration>
</system.identitymodel.services>
我们需要使用 ws-trust 端点来实现我们的目标,这是 Login
/Logoff
方法的代码(AccountController.cs)
//
// POST: /Account/Login
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public ActionResult Login(LoginModel model, string returnUrl)
{
//if (ModelState.IsValid && WebSecurity.Login
(model.UserName, model.Password, persistCookie: model.RememberMe))
//{
// return RedirectToLocal(returnUrl);
//}
if (ModelState.IsValid)
{
var cp = GetClaimsFromIdentityServer(model.UserName, model.Password);
if (cp != null)
{
//All set so now create a SessionSecurityToken
var token = new SessionSecurityToken(cp) {
IsReferenceMode = true //this is
//important.this is how you say create
//the token in reference mode meaning
//your session cookie will contain only a
//referenceid(which is very small) and
//all claims will be stored on the server
};
FederatedAuthentication.WSFederationAuthenticationModule.
SetPrincipalAndWriteSessionToken(token, true);
return RedirectToLocal(returnUrl);
}
}
// If we got this far, something failed, redisplay form
ModelState.AddModelError
("", "The user name or password provided is incorrect.");
return View(model);
}
private ClaimsPrincipal GetClaimsFromIdentityServer(string username, string password)
{
const string WS_TRUST_END_POINT = "https://{0}/issue/wstrust/mixed/username";
var sts =ConfigurationManager.AppSettings["IdentityServer"];
var factory = new System.ServiceModel.Security.WSTrustChannelFactory
(new UserNameWSTrustBinding(SecurityMode.TransportWithMessageCredential),
string.Format(WS_TRUST_END_POINT, sts));
factory.TrustVersion = TrustVersion.WSTrust13;
factory.Credentials.UserName.UserName = username;
factory.Credentials.UserName.Password = password;
var rst = new System.IdentityModel.Protocols.WSTrust.RequestSecurityToken
{
RequestType = RequestTypes.Issue,
KeyType = KeyTypes.Bearer,
TokenType = TokenTypes.JsonWebToken, //yes we need only
//Json Web Tokens as they are more compact then default SAML tokens.
//It matters as we have to send the token to your
//API Services with each API request
AppliesTo = new EndpointReference
("http://www.xyz.com/yourapp") //this is the RP
//you created in Identity Server Admin UI
};
var st = factory.CreateChannel().Issue(rst);
var token = st as GenericXmlSecurityToken;
var handlers = FederatedAuthentication.FederationConfiguration.
IdentityConfiguration.SecurityTokenHandlers;
var jwtToken = handlers.ReadToken(new XmlTextReader
(new StringReader(token.TokenXml.OuterXml))) as JwtSecurityToken;
var identity = handlers.ValidateToken(jwtToken).First();
var principal = new ClaimsPrincipal(identity);
return principal;
}
//
// POST: /Account/LogOff
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult LogOff()
{
//WebSecurity.Logout();
System.Web.HttpContext.Current.Session.Clear();
FormsAuthentication.SignOut();
try
{
FederatedAuthentication.SessionAuthenticationModule.SignOut();
FederatedAuthentication.SessionAuthenticationModule.DeleteSessionTokenCookie();
FederatedAuthentication.WSFederationAuthenticationModule.SignOut(false);
}
catch (Exception)
{
//we can ignore
}
return RedirectToAction("Index", "Home");
}
将令牌发送到 API 服务
我使用 RestSharp
调用我们的 API 服务器,它工作得相当好,并且比 HttpClient
简单得多。示例如下
public ActionResult Index()
{
ViewBag.Message = "Called the API
along with user claims and got the response below";
//Calling the Web Api hosted on a remote server.
//TokenAuthenticator will take care of adding the
//token to the HTTP Header before making the request to the server.
var restClient = new RestClient(ConfigurationManager.AppSettings
["ApiServer"]) { Authenticator = new TokenAuthenticator() };
string url = "api/Values";
var request = new RestRequest(url, Method.GET);
var data = restClient.Execute<dynamic>(request);
ViewBag.Data = JValue.Parse(data.Content);
var email = ViewBag.Data.Email;
return View();
}
TokenAuthenticator.cs 的源代码如下
public class TokenAuthenticator : IAuthenticator
{
public void Authenticate(IRestClient client, IRestRequest request)
{
var token = ClaimsPrincipal.Current.GetTokenString();
if (!string.IsNullOrEmpty(token))
{
var header = new AuthenticationHeaderValue("Bearer", token);
request.AddHeader("Authorization", header.ToString());
}
}
}
API 服务 - 配置
Web.Config 条目如下所示
<system.identitymodel>
<identityconfiguration savebootstrapcontext="true">
<audienceuris>
<add value="http://www.xyz.com/yourapp">
<add value="http://www.xyz.com/yourapp/api">
</audienceuris>
<securitytokenhandlers>
<add type="System.IdentityModel.Tokens.JwtSecurityTokenHandler,
System.IdentityModel.Tokens.Jwt, Version=2.0.0.0, Culture=neutral,
PublicKeyToken=31bf3856ad364e35">
<securitytokenhandlerconfiguration>
<issuertokenresolver type="System.IdentityModel.
Tokens.X509CertificateStoreTokenResolver, System.IdentityModel,
Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
<certificatevalidation certificatevalidationmode="PeerOrChainTrust"
trustedstorelocation="LocalMachine" revocationmode="NoCheck">
<issuernameregistry type="System.IdentityModel.Tokens.
ValidatingIssuerNameRegistry, System.IdentityModel.Tokens.ValidatingIssuerNameRegistry,
Version=2.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35">
<authority name="http://identityserver.v2.xyz.com/trust">
<keys>
<add thumbprint="C2B2219F3CAC53658E796C0402360D90AEFA08FC">
</keys>
<validissuers>
<add name="http://identityserver.v2.xyz.com/trust">
</validissuers>
</authority>
</issuernameregistry>
</securitytokenhandlerconfiguration>
</securitytokenhandlers>
<claimsauthorizationmanager
type="IdentityServer.Demo.Common.Security.CustomAuthorizationManager,
IdentityServer.Demo.Common">
</identityconfiguration>
</system.identitymodel>
<system.identitymodel.services>
<federationconfiguration>
<cookiehandler mode="Default" requiressl="false">
<wsfederation realm="http://www.xyz.com/yourapp"
issuer="https://sts.xyz.com/issue/wsfed"
passiveredirectenabled="false" requirehttps="true">
</federationconfiguration>
</system.identitymodel.services>
在 Global.asax(Application_Start
)中,我们需要添加一个 messageHandler
,以便在处理每个 HTTP 调用之前对其进行拦截。此处理程序将检查 HTTP Authorization 头并对其进行解密,然后用所有声明填充当前用户主体。
protected void Application_Start()
{
//Add a handler to interrupt the http call and read the
//http authorization header and populate the current user principal
GlobalConfiguration.Configuration.MessageHandlers.Add(new TokenValidationHandler());
AreaRegistration.RegisterAllAreas();
WebApiConfig.Register(GlobalConfiguration.Configuration);
FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
RouteConfig.RegisterRoutes(RouteTable.Routes);
BundleConfig.RegisterBundles(BundleTable.Bundles);
}
如何检查声明
我更喜欢使用 Thinktecture.IdentityModal
(可在 Nuget 上获取)中的 ClaimsAuthorizeAttribute
。然后它将非常简单,如下所示:
[ClaimsAuthorize(IdentityServer.Demo.Common.Security.Claims.ClaimTypes.Manager)]
public string Get(int id)
{
return "value";
}
您也可以在代码中检查声明。请检查 ValuesController
中的 Get
方法。我还提供了一些扩展方法,以便更轻松地检查声明。
当您的会话 Cookie 变得太大时
一旦您的应用程序变得复杂,要处理的声明数量也会增加。默认情况下,所有声明都存储在会话 Cookie 中,而 Safari 等浏览器对 Cookie 的大小有限制。所以有一天,当您为应用程序添加更多声明时,您将开始收到序列化错误。这是因为只有部分 Cookie 会发送回服务器,服务器不知道如何处理它。因此,这个问题的解决方案是创建一个“引用”模式的安全令牌。这意味着将令牌存储在服务器上,而只在 Cookie 中存储一个引用会话 ID。请看下图。Cookie 大小仅为几个字节
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public ActionResult Login(LoginModel model, string returnUrl)
{
//if (ModelState.IsValid && WebSecurity.Login
//(model.UserName, model.Password, persistCookie: model.RememberMe))
//{
// return RedirectToLocal(returnUrl);
//}
if (ModelState.IsValid)
{
var cp = GetClaimsFromIdentityServer(model.UserName, model.Password);
if (cp != null)
{
//All set so now create a SessionSecurityToken
var token = new SessionSecurityToken(cp) {
IsReferenceMode = true //this is important.
//this is how you say create the token in
//reference mode meaning the your session cookie
//will contain only a referenceid(which is very small)
//and all claims will be stored on the server
};
FederatedAuthentication.WSFederationAuthenticationModule.
SetPrincipalAndWriteSessionToken(token, true);
return RedirectToLocal(returnUrl);
}
}
// If we got this far, something failed, redisplay form
ModelState.AddModelError
("", "The user name or password provided is incorrect.");
return View(model);
}
会话令牌缓存
只要您处于单服务器实例场景,引用模式就能正常工作,但当您处于 Web 场场景时,它将无法工作,因为默认情况下缓存的令牌存储在服务器内存中。这个问题的解决方案是将令牌缓存到自定义数据存储中。再次,Thinktecture.IdentityModel
是我们的朋友。我们所要做的就是实现一个简单的接口,并在 Global.asax 中添加几行代码。我提供了将令牌缓存到 SQL Server/MongoDB/AppFabric 的实现。因此,您可以选择您想要的任何一种。在 Global.asax 中,您需要在 Init
方法中添加以下几行代码
protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();
WebApiConfig.Register(GlobalConfiguration.Configuration);
FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
RouteConfig.RegisterRoutes(RouteTable.Routes);
BundleConfig.RegisterBundles(BundleTable.Bundles);
AuthConfig.RegisterAuth();
AntiForgeryConfig.UniqueClaimTypeIdentifier =
ClaimTypes.Name; //http://brockallen.com/2012/07/08/mvc-4-antiforgerytoken-and-claims/
//Configure the SessionSecurityTokenCache Provider
//PassiveSessionConfiguration.ConfigureSessionCache(new SqlTokenCacheRepository());
PassiveSessionConfiguration.ConfigureSessionCache(new MongoTokenCacheRepository());
//PassiveSessionConfiguration.ConfigureSessionCache(new AppFabricTokenCacheRepository());
//PassiveSessionConfiguration.ConfigureSessionCache(new SessionTokenCacheRepository());
}
public override void Init()
{
PassiveModuleConfiguration.CacheSessionsOnServer();
PassiveModuleConfiguration.SuppressLoginRedirectsForApiCalls();
base.Init();
}
常见错误
在进行此练习时,您会遇到一些常见错误及其解决方案。WIF10201:找不到安全令牌 'System.IdentityModel.Tokens.X509SecurityToken' 和发布者 'http://identityserver.v2.xyz.com/trust' 的有效密钥映射。
请确保您在 web.config 的 issuerNameRegistry
条目中使用了正确的证书指纹。
<issuernameregistry type="System.IdentityModel.Tokens.ValidatingIssuerNameRegistry,
System.IdentityModel.Tokens.ValidatingIssuerNameRegistry, Version=2.0.0.0,
Culture=neutral, PublicKeyToken=31bf3856ad364e35">
<authority name="http://identityserver.v2.xyz.com/trust">
<keys>
<add thumbprint="C2B2219F3CAC53658E796C0402360D90AEFA08FC">
</keys>
<validissuers>
<add name="http://identityserver.v2.xyz.com/trust">
</validissuers>
</authority>
</issuernameregistry>
ID4243:无法创建 SecurityToken。在令牌缓存中找不到令牌,并且在上下文中找不到 Cookie。
我在开发过程中经常遇到此错误,当时我不断停止/启动开发 Web 服务器。它说明找到一个会话 Cookie,但在服务器令牌缓存中没有与该 Cookie 对应的项。您只需删除该域的所有 Cookie 即可解决问题。
关注点
我已将所有通用代码放在一个名为 IdenityServer.Demo.Common
的单独项目中,并在我认为必要的地方添加了注释。我通过阅读许多博客和 MSDN 文档收集了所有这些信息,并提供了大部分参考资料。如果我遗漏了任何信息,那是完全无意的,如果您告知我,我很乐意添加参考。请告诉我您的评论和反馈。
您需要的 Nuget 包
- Thinktecture.IdentityServer.Core (用于扩展 Identity Server)
Thinktecture.IdentityModel
- System.IdentityModel.Tokens.Jwt
- System.IdentityModel.Tokens.ValidatingIssuerNameRegistry
- ServerAppFabric.Client (如果您想使用 AppFabric 进行缓存)
- RestSharp
- Newtonsoft.Json
- mongocsharpdriver (如果您想使用 MongoDB 进行缓存)
参考文献
- http://vimeo.com/51088126
- http://garymcallisteronline.blogspot.com/2013/03/extending-thinktectureidentity-server.html
- http://www.garycrawford.co.uk/complex-claim-values-with-wif/
- http://www.cloudidentity.com/blog/2012/11/30/using-the-bootstrapcontext-property-in-net-4-5-2/
- http://slproweb.com/products/Win32OpenSSL.html
历史
- 初始版本 - 2013/11/13