65.9K
CodeProject 正在变化。 阅读更多。
Home

Thinktecture Identity Server - 配置、定制

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.91/5 (22投票s)

2013 年 11 月 17 日

CPOL

6分钟阅读

viewsIcon

167995

downloadIcon

1947

安装、扩展 Identity Server 并实现会话令牌缓存

Identity Server

引言

在我最近的一个项目中,我们希望使用 Thinktecture Identity Server 来满足我们的身份验证/授权需求。在这个过程中,我遇到了许多与配置相关的问题和发现。我只想分享我的发现,希望对其他开发者有所帮助。

上图展示了我想要实现的目标,我认为这就像一个典型的 Web 应用程序。

我想要实现的目标

  1. 安装和配置 Identity Server
  2. 扩展 Identity Server 以使用我们自己的数据存储来检查用户凭据并获取用户声明
  3. 实现 SecurityToken 缓存
  4. 将令牌传递给我们的 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 -Configuration

Identity Server -Configuration

Identity Server -Configuration

扩展 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.asaxApplication_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 大小仅为几个字节

Cookie Size

[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.configissuerNameRegistry 条目中使用了正确的证书指纹。

<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>	

Security Token Error

ID4243:无法创建 SecurityToken。在令牌缓存中找不到令牌,并且在上下文中找不到 Cookie。

我在开发过程中经常遇到此错误,当时我不断停止/启动开发 Web 服务器。它说明找到一个会话 Cookie,但在服务器令牌缓存中没有与该 Cookie 对应的项。您只需删除该域的所有 Cookie 即可解决问题。

Security Token Error

关注点

我已将所有通用代码放在一个名为 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 进行缓存)

参考文献

历史

  • 初始版本 - 2013/11/13
© . All rights reserved.