SharePoint 프로젝트용 Azure 사용자 지정 클레임 공급자(3부)

최초 문서 게시일: 2012년 2월 21일 화요일

이 시리즈의 1부에서는 이 프로젝트의 목표(간략하게 설명하자면 SharePoint 사용자 지정 클레임 공급자에 대해 Windows Azure 테이블 저장소를 데이터 저장소로 사용하는 것)에 대해 대략적으로 설명했습니다. 클레임 공급자는 CASI Kit(영문일 수 있음)를 사용하여 사용자 선택(주소록) 및 입력 컨트롤 이름 확인 기능을 제공하는 데 필요한 데이터를 Windows Azure에서 검색합니다. 

이 시리즈의 2부에서는 클라우드에서 실행되는 모든 구성 요소에 대해 차례로 살펴봅니다. 이러한 구성 요소로는 Azure 테이블 저장소 및 큐 작업에 사용되는 데이터 클래스, 큐의 항목을 읽고 테이블 저장소를 채우는 작업자 역할, 그리고 클라이언트 응용 프로그램이 큐에 새 항목을 만들고 모든 표준 SharePoint 사용자 선택 작업(지원되는 클레임 유형 목록 제공, 클레임 값 검색, 클레임 확인)을 수행할 수 있도록 하는 WCF 프런트 엔드가 포함됩니다.

시리즈의 마지막 문서인 이 게시물에서는 SharePoint 쪽에서 사용되는 다양한 구성 요소를 차례로 살펴봅니다. 여기에는 큐에 항목을 추가하고 Azure 테이블 저장소를 호출하기 위해 CASI Kit를 사용하여 작성된 사용자 지정 구성 요소가 포함됩니다. 또한 CASI Kit 구성 요소를 사용하여 SharePoint를 이러한 Azure 기능과 연결하는 사용자 지정 클레임 공급자도 포함됩니다.

먼저 사용자 지정 CASI Kit 구성 요소를 잠시 살펴보겠습니다. CASI Kit에 대해서는 이 블로그에서 폭넓게 다루었기 때문에 이 게시물에서 자세히 설명하지는 않겠습니다. 이 구성 요소에 대한 설명은 CASI Kit 시리즈의 3부에 나와 있습니다. 여기서는 간단하게 새 Windows 클래스 라이브러리 프로젝트를 작성해 보았습니다. 그런 다음 CASI Kit 기본 클래스 어셈블리와 기타 필수 .NET 어셈블리(3부에서 설명함)에 대한 참조를 추가했습니다. 그리고 이 프로젝트의 2부에서 만든 WCF 끝점에 대한 서비스 참조를 프로젝트에 추가했습니다. 마지막으로, 프로젝트에 새 클래스를 추가하고 CASI Kit 기본 클래스를 상속하도록 지정했으며 ExecuteRequest 메서드를 재정의하는 코드를 추가했습니다. CASI Kit 시리즈에서 이미 확인하신 분도 계시겠지만, ExecuteRequest를 재정의하는 코드는 다음과 같습니다.

       public class DataSource : AzureConnect.WcfConfig

       {

              public override bool ExecuteRequest()

              {

                     try

                     {

                           //create the proxy instance with bindings and endpoint the base class

                           //configuration control has created for this

                           AzureClaims.AzureClaimsClient cust =

                                  new AzureClaims.AzureClaimsClient(this.FedBinding,

                                   this.WcfEndpointAddress);

 

                           //configure the channel so we can call it with

                           //FederatedClientCredentials.

                           SPChannelFactoryOperations.ConfigureCredentials<AzureClaims.IAzureClaims>

                                   (cust.ChannelFactory,

                                   Microsoft.SharePoint.SPServiceAuthenticationMode.Claims);

 

                           //create a channel to the WCF endpoint using the

                           //token and claims of the current user

                           AzureClaims.IAzureClaims claimsWCF =

                                  SPChannelFactoryOperations.CreateChannelActingAsLoggedOnUser

                                  <AzureClaims.IAzureClaims>(cust.ChannelFactory,

                                  this.WcfEndpointAddress,

                                  new Uri(this.WcfEndpointAddress.Uri.AbsoluteUri));

 

                           //set the client property for the base class

                           this.WcfClientProxy = claimsWCF;

                     }

                     catch (Exception ex)

                     {

                           Debug.WriteLine(ex.Message);

                     }

 

                     //now that the configuration is complete, call the method

                     return base.ExecuteRequest();

              }

       }

 

제가 만든 서비스 참조인 "AzureClaims"는 Azure의 WCF 프로젝트에서 정의된 IAzureClaims 클레임 인터페이스를 사용합니다. 이전에 CASI Kit 시리즈에서 설명한 것처럼 이 항목은 상용구 코드이므로 저는 WCF 응용 프로그램에서 표시되는 인터페이스 및 클래스의 이름만 연결했습니다. 그리고 역시 CASI Kit 시리즈에서 설명한 것처럼 AzureClaimProvider.aspx라는 ASPX 페이지를 만들었습니다. 이 작업에서는 CASI Kit 시리즈의 3부에서 설명한 코드를 단순히 복사하여 붙여 넣었으며, 클래스 및 이 클래스에 연결할 수 있는 끝점의 이름만 교체했습니다. 사용자 지정 CASI Kit 구성 요소의 ASPX 페이지에 있는 컨트롤 태그는 다음과 같습니다.

<AzWcf:DataSource runat="server" id="wcf" WcfUrl="https://spsazure.vbtoys.com/ AzureClaims.svc" OutputType="Page" MethodName="GetClaimTypes" AccessDeniedMessage="" />

 

여기서 주목할 점은 cloudapp.net의 Azure 응용 프로그램을 가리키는 "spsazure.vbtoys.com"에 대해 CNAME 레코드를 만들었다는 것입니다. 여기에 대한 설명도 CASI Kit의 3부에 나와 있습니다. 페이지가 호출할 기본 MethodName은 GetClaimTypes로 설정했습니다. 이 메서드는 매개 변수를 사용하지 않으며 Azure 클레임 공급자가 지원하는 클레임 유형 목록을 반환합니다. 따라서 적절한 테스트를 통해 Azure 응용 프로그램과 SharePoint 간의 연결 유효성을 검사할 수 있습니다. https://anySharePointsite/_layouts/AzureClaimProvider.aspx로 이동했을 때 페이지에 데이터가 표시되면 모든 항목이 올바르게 구성된 것입니다. 전역 어셈블리 캐시에 어셈블리를 추가하고 SharePoint의 _layouts 디렉터리에 페이지를 배포하여 프로젝트를 배포한 후에는 사이트 중 하나의 페이지를 방문하여 데이터가 반환되는지 확인함으로써 SharePoint와 Azure 간의 연결이 작동하는지를 파악했습니다.

이처럼 모든 항목이 올바르게 구성되었는지 확인한 후에는 마지막으로 프로젝트에 대해 다음과 같은 두 가지 흥미로운 작업을 수행합니다.

  1. 1.       Azure 큐로 새 사용자에 대한 정보를 보내는 구성 요소 만들기
  2. 2.       사용자 지정 CASI Kit 구성 요소를 사용하여 클레임 유형, 이름 확인 및 클레임 검색 기능을 제공하는 사용자 지정 클레임 공급자 만들기

여기서 잠시 이전 단계를 다시 살펴보겠습니다. 이 게시물에서 설명하는 프로젝트에서는 콘텐츠를 최대한 신속하게 제공하고자 합니다. 따라서 새 웹 응용 프로그램을 만든 다음 익명 액세스를 사용하도록 설정했습니다. 아시다시피 익명 액세스는 웹 응용 프로그램 수준에서 사용하도록 설정해도 사이트 모음 수준에서는 사용하도록 설정되지 않습니다. 그러므로 이 시나리오에서는 루트 사이트 모음에 한해서도 익명 액세스를 사용하도록 설정하여 사이트의 모든 항목에 대한 액세스 권한을 부여했습니다. 구성원 전용 정보를 포함하는 다른 모든 사이트 모음에서는 익명 액세스가 사용하도록 설정되지 않았으므로 이러한 사이트 모음에 참가하려는 사용자는 해당 권한을 부여받아야 합니다.

다음으로는 사이트를 사용할 ID를 관리하는 방법에 대해 고려해야 합니다. 물론 여기서는 단순히 ID 관리를 수행하지는 않습니다. 여러 방법을 통해 계정을 Azure에 동기화하는 등의 작업을 직접 수행할 수도 있지만, 이 시리즈의 1부에서 설명한 것처럼 이미 그러한 작업을 수행하는 공급자가 많이 있으므로 여기서도 공급자가 해당 작업을 수행하도록 합니다. 여기서는 또 다른 Microsoft 클라우드 서비스인 ACS(액세스 제어 서비스)를 활용했습니다. 한 마디로 설명하자면, ACS는 SharePoint에 대해 ID 공급자 역할을 합니다. 따라서 이 POC용으로 만든 ACS 인스턴스와 SharePoint 팜 간에 트러스트를 작성했습니다. ACS에서는 SharePoint를 신뢰 당사자로 추가하여 ACS가 인증된 사용자를 보낼 위치를 확인할 수 있도록 했습니다. 또한 ACS 내에서 사용자가 Gmail, Yahoo 또는 Facebook 계정을 사용하여 로그인할 수 있도록 구성했습니다. 사용자가 로그인하고 나면 ACS는 여기서 사용할 단일 클레임(전자 메일 주소)을 다시 가져와 SharePoint로 다시 보냅니다.

지금까지 배경 정보에 대해 간략하게 살펴보았습니다. 요약하자면 Azure가 데이터 작업을 위한 테이블 저장소와 큐를 제공하고, ACS는 인증 서비스를 제공하며, CASI Kit는 데이터에 대해 사용할 수 있는 구성 요소를 제공합니다.

그러면 지금까지 설명한 구성 요소를 어떻게 사용하면 될까요? 프로세스를 간단하게 유지하기 위해 사용자를 Azure 큐에 추가하는 웹 파트를 작성했습니다. 이 웹 파트는 요청이 인증되었는지 여부를 확인합니다. 즉, 사용자가 익명 사이트에 표시되는 로그인 링크를 클릭하고 위에서 설명한 공급자 중 하나에 로그인하면 ACS에서 클레임 정보를 다시 보냅니다. 요청이 인증되지 않았으면 웹 파트는 아무런 작업도 수행하지 않습니다. 그러나 요청이 인증되었으면 웹 파트는 단추를 렌더링하며, 이 단추는 클릭하는 경우 사용자의 전자 메일 클레임을 가져와 Azure 큐에 추가합니다. 앞 단락에서 "잠시 이전 단계를 다시 살펴본다"고 했던 작업이 바로 이 부분입니다. POC의 경우에는 이 방식이 문제 없이 작동합니다. 그러나 이 요청을 처리할 수 있는 다른 방식도 있습니다. 예를 들어 SharePoint 목록에 정보를 작성할 수도 있고, CASI Kit에서 효율적으로 사용할 수 있는 사용자 지정 타이머 작업을 작성한 다음 해당 목록에서 새 요청을 주기적으로 처리할 수도 있습니다. 또한 SPWorkItem을 사용하여 요청을 나중에 프로세스에 대기시킬 수도 있습니다. 요청을 목록에 저장한 후에, 특정 승인 프로세스를 거칠 수 있으며 요청이 승인되면 사용자 지정 워크플로 작업을 통해 CASI Kit를 호출하여 세부 정보를 Azure 큐로 밀어 넣는 사용자 지정 워크플로를 추가할 수도 있습니다. 즉, 동일한 작업을 수행하기 위해 이처럼 다양한 방법을 유동적으로 활용하고 사용자 지정할 수 있습니다. 실제로 이 프로젝트에서도 코드의 다른 버전, 즉 요청을 사용자 지정 목록에 쓴 다음 특정 지점에서 비동기 방식으로 처리하고 Azure 큐에 데이터를 추가한 후에 하위 사이트 중 하나의 Visitors 그룹에 계정을 자동으로 추가하는 버전을 작성할 수 있습니다. 이렇게 하면 사용자가 자동으로 등록되어 사이트에 즉시 액세스할 수 있습니다. 해당 방법에 대해서는 기회가 되면 다른 게시물에서 설명하겠습니다.

위에서 설명이 길어졌는데 여기서는 사용자가 로그인한 경우 단추를 클릭할 수 있도록 하고, 사용자 지정 CASI Kit 구성 요소를 사용하여 WCF 끝점을 호출한 다음 Azure 큐에 정보를 추가합니다. 웹 파트의 간단한 코드(CASI Kit에 포함되어 있음)는 다음과 같습니다

public class AddToAzureWP : WebPart

{

 

       //button whose click event we need to track so that we can

       //add the user to Azure

       Button addBtn = null;

       Label statusLbl = null;

 

       protected override void CreateChildControls()

       {

              if (this.Page.Request.IsAuthenticated)

              {

                     addBtn = new Button();

                     addBtn.Text = "Request Membership";

                     addBtn.Click += new EventHandler(addBtn_Click);

                     this.Controls.Add(addBtn);

 

                     statusLbl = new Label();

                     this.Controls.Add(statusLbl);

              }

       }

 

       void addBtn_Click(object sender, EventArgs e)

       {

              try

              {

                     //look for the claims identity

                     IClaimsPrincipal cp = Page.User as IClaimsPrincipal;

 

                     if (cp != null)

                     {

                           //get the claims identity so we can enum claims

                           IClaimsIdentity ci = (IClaimsIdentity)cp.Identity;

 

                           //look for the email claim

                           //see if there are claims present before running through this

                           if (ci.Claims.Count > 0)

                           {

                                  //look for the email address claim

                                  var eClaim = from Claim c in ci.Claims

                                         where c.ClaimType == "https://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress"

                                         select c;

 

                                  Claim ret = eClaim.FirstOrDefault<Claim>();

 

                                  if (ret != null)

                                  {

                                         //create the string we're going to send to the Azure queue:  claim, value, and display name

                                         //note that I'm using "#" as delimiters because there is only one parameter, and CASI Kit

                                         //uses ; as a delimiter so a different value is needed.  If ; were used CASI would try and

                                         //make it three parameters, when in reality it's only one

                                         string qValue = ret.ClaimType + "#" + ret.Value + "#" + "Email";

 

                                         //create the connection to Azure and upload

                                         //create an instance of the control

                                         AzureClaimProvider.DataSource cfgCtrl = new AzureClaimProvider.DataSource();

 

                                         //set the properties to retrieve data; must configure cache properties since we're using it programmatically

                                         //cache is not actually used in this case though

                                         cfgCtrl.WcfUrl = AzureCCP.SVC_URI;

                                         cfgCtrl.OutputType = AzureConnect.WcfConfig.DataOutputType.None;

                                         cfgCtrl.MethodName = "AddClaimsToQueue";

                                         cfgCtrl.MethodParams = qValue;

                                         cfgCtrl.ServerCacheTime = 10;

                                         cfgCtrl.ServerCacheName = ret.Value;

                                         cfgCtrl.SharePointClaimsSiteUrl = this.Page.Request.Url.ToString();

 

                                         //execute the method

                                         bool success = cfgCtrl.ExecuteRequest();

 

                                         if (success)

                                         {

                                                //if it worked tell the user

                                                statusLbl.Text = "<p>Your information was successfully added.  You can now contact any of " +

                                                       "the other Partner Members or our Support staff to get access rights to Partner " +

                                                       "content.  Please note that it takes up to 15 minutes for your request to be " +

                                                       "processed.</p>";

                                         }

                                         else

                                         {

                                                statusLbl.Text = "<p>There was a problem adding your info to Azure; please try again later or " +

                                                       "contact Support if the problem persists.</p>";

                                         }

                                  }

                           }

                     }

              }

              catch (Exception ex)

              {

                     statusLbl.Text = "There was a problem adding your info to Azure; please try again later or " +

                           "contact Support if the problem persists.";

                     Debug.WriteLine(ex.Message);

              }

       }

}

 

그러면 위의 코드에 대해 간략하게 살펴보겠습니다. 먼저 요청이 인증되었는지를 확인한 다음 인증되었으면 페이지에 단추를 추가하고 단추 클릭 이벤트에 대한 이벤트 처리기를 추가합니다. 단추 클릭 이벤트 처리기에서는 현재 사용자에 대한 IClaimsPrincipal 참조를 가져온 후 사용자의 클레임 컬렉션을 확인합니다. 다음으로 클레임 컬렉션에 대해 LINQ 쿼리를 실행하여 전자 메일 클레임(SPTrustedIdentityTokenIssuer에 대한 ID 클레임)을 찾습니다. 전자 메일 클레임이 발견되면 클레임 유형, 클레임 값 및 클레임 이름을 사용하여 연결된 문자열을 만듭니다. 이 작업 역시 이 시나리오에서 반드시 수행해야 하는 것은 아니지만, 이 프로젝트를 보다 일반적인 시나리오에서 사용할 수 있도록 하기 위해 이러한 방식으로 코드를 작성했습니다. 이 연결된 문자열은 데이터를 Azure 큐에 추가하는 WCF의 메서드에 대한 값입니다. 그런 후에 사용자 지정 CASI Kit 구성 요소 인스턴스를 만들어 큐에 데이터를 추가하는 WCF 메서드를 호출하도록 구성하고, 마지막으로 ExecuteRequest 메서드를 호출하여 실제로 데이터를 가져옵니다. 

데이터를 큐에 정상적으로 추가했음을 나타내는 응답을 받으면 사용자에게 작업이 성공했음을 알리고, 그렇지 않으면 문제가 발생했으므로 나중에 다시 확인해야 할 수 있음을 알립니다. 물론 실제 시나리오에서는 다양한 오류가 기록될 것이므로 정확한 실제 상황 및 이유를 추적할 수 있습니다. 이 게시물에서 사용하는 간단한 코드의 경우에도 CASI Kit는 ULS 로그의 SPMonitoredScope에 모든 오류 정보를 기록하므로 요청에 대해 수행되는 모든 작업에는 고유한 상관 관계 ID가 지정됩니다. 이 ID를 통해 요청과 연관된 모든 작업을 확인할 수 있습니다. 여기까지는 문제가 없습니다.

지금까지 모든 구성 요소에 대해 살펴보았으며, 데이터를 Azure 큐에 추가한 다음 작업자 프로세스를 통해 끌어와서 테이블 저장소에 추가하는 방법을 살펴보았습니다. 이것이 프로젝트의 최종 목표이며, 이제 사용자 지정 클레임 공급자에 대해 살펴볼 수 있습니다. 사용자 지정 클레임 공급자는 CASI Kit를 사용하여 현재 사용 중인 Azure 테이블 저장소를 호출 및 쿼리합니다. 그러면 사용자 지정 클레임 공급자의 몇 가지 흥미로운 측면을 살펴보겠습니다.

먼저 두 가지 클래스 수준 특성을 확인해 보겠습니다.

//the WCF endpoint that we'll use to connect for address book functions

//test url:  https://az1.vbtoys.com/AzureClaimsWCF/AzureClaims.svc

//production url:  https://spsazure.vbtoys.com/AzureClaims.svc

public static string SVC_URI = "https://spsazure.vbtoys.com/AzureClaims.svc";

 

//the identity claimtype value

private const string IDENTITY_CLAIM =

       "https://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress";

 

//the collection of claim type we support; it won't change over the course of

//the STS (w3wp.exe) life time so we'll cache it once we get it

AzureClaimProvider.AzureClaims.ClaimTypeCollection AzureClaimTypes =

       new AzureClaimProvider.AzureClaims.ClaimTypeCollection();

 

위의 코드에서는 먼저 상수를 사용하여 CASI Kit가 연결해야 하는 WCF 끝점을 참조합니다. 코드에서 보시다시피 테스트 끝점과 프로덕션 끝점이 모두 포함되어 있습니다. 이 게시물의 사용자 지정 클레임 공급자에서와 같이 CASI Kit를 프로그래밍 방식으로 사용할 때는 CASI Kit가 연결해야 하는 WCF 끝점을 항상 지정해야 합니다.

다음으로, 앞에서 설명한 것처럼 전자 메일 클레임을 ID 클레임으로 사용합니다. 공급자 전체에서 이 클레임을 여러 번 참조할 것이므로 클래스 수준에서 상수를 연결했습니다.

마지막으로 AzureClaimTypes 컬렉션을 추가했습니다. 컬렉션을 사용하는 이유에 대해서는 이 시리즈의 2부에서 설명했으며, FillHierarchy 메서드를 호출할 때마다 해당 정보를 다시 가져오지 않아도 되도록 클래스 수준에서 해당 컬렉션을 저장합니다. Azure를 호출하면 작업이 복잡해지므로 가능한 경우에는 호출을 최소화합니다.

그 다음 코드 청크가 아래에 나와 있습니다.

internal static string ProviderDisplayName

{

       get

       {

              return "AzureCustomClaimsProvider";

       }

}

 

internal static string ProviderInternalName

{

       get

       {

              return "AzureCustomClaimsProvider";

       }

}

 

//*******************************************************************

//USE THIS PROPERTY NOW WHEN CREATING THE CLAIM FOR THE PICKERENTITY

internal static string SPTrustedIdentityTokenIssuerName

{

       get

       {

              return "SPS ACS";

       }

}

 

 

public override string Name

{

       get

       {

              return ProviderInternalName;

       }

}

 

여기서 이 코드에 대해 특별히 설명을 하는 이유는, 공급자가 ID 클레임을 발급하므로 SPTrustedIdentityTokenIssuer의 기본 공급자여야 하기 때문입니다. 이 게시물에서는 공급자를 기본 공급자로 지정하는 방법에 대해 설명하지 않으며, 블로그의 다른 게시물에서 해당 설명을 확인할 수 있습니다. 이 작업에서 기억해야 하는 가장 중요한 점은 공급자에 대해 사용하는 이름과 SPTrustedIdentityTokenIssuer에 대해 사용하는 이름 간에 밀접한 관계가 있어야 한다는 것입니다. 여기서 ProviderInternalName에 대해 사용한 값은 SPTrustedIdentityTokenIssuer의 ClaimProviderName 속성에 연결해야 하는 이름입니다. 또한 사용자에 대해 ID 클레임을 만들 때도 SPTrustedIdentityTokenIssuer의 이름을 사용해야 합니다. 따라서 "SPS ACS"라는 SPTrustedIdentityTokenIssuer를 만들어 SPTrustedIdentityTokenIssuerName 속성에 추가했습니다. 위 코드를 발췌한 이유는 이 점을 설명하기 위해서입니다.

이 공급자에서는 클레임 확대를 수행하지 않으므로 FillClaimTypes, FillClaimValueTypes 또는 FillEntityTypes를 재정의하는 코드는 작성하지 않았습니다. 다음 코드 청크는 지원할 클레임 유형을 SharePoint에 알려 주는 FillHierarchy입니다.

try

{

       if (

               (AzureClaimTypes.ClaimTypes == null) ||

               (AzureClaimTypes.ClaimTypes.Count() == 0)

       )

       {

              //create an instance of the control

              AzureClaimProvider.DataSource cfgCtrl = new AzureClaimProvider.DataSource();

 

              //set the properties to retrieve data; must configure cache properties since we're using it programmatically

              //cache is not actually used in this case though

              cfgCtrl.WcfUrl = SVC_URI;

              cfgCtrl.OutputType = AzureConnect.WcfConfig.DataOutputType.None;

              cfgCtrl.MethodName = "GetClaimTypes";

              cfgCtrl.ServerCacheTime = 10;

              cfgCtrl.ServerCacheName = "GetClaimTypes";

              cfgCtrl.SharePointClaimsSiteUrl = context.AbsoluteUri;

 

              //execute the method

              bool success = cfgCtrl.ExecuteRequest();

 

              if (success)

              {

                     //if it worked, get the list of claim types out

                     AzureClaimTypes =

                                                (AzureClaimProvider.AzureClaims.ClaimTypeCollection)cfgCtrl.QueryResultsObject;

              }

       }

 

       //make sure picker is asking for the type of entity we return; site collection admin won't for example

       if (!EntityTypesContain(entityTypes, SPClaimEntityTypes.User))

              return;

 

       //at this point we have whatever claim types we're going to have, so add them to the hierarchy

       //check to see if the hierarchyNodeID is null; it will be when the control

       //is first loaded but if a user clicks on one of the nodes it will return

       //the key of the node that was clicked on.  This lets you build out a

       //hierarchy as a user clicks on something, rather than all at once

       if (

               (string.IsNullOrEmpty(hierarchyNodeID)) &&

               (AzureClaimTypes.ClaimTypes.Count() > 0)

              )

       {

              //enumerate through each claim type

              foreach (AzureClaimProvider.AzureClaims.ClaimType clm in AzureClaimTypes.ClaimTypes)

              {

                     //when it first loads add all our nodes

                     hierarchy.AddChild(new

                                                Microsoft.SharePoint.WebControls.SPProviderHierarchyNode(

                                   ProviderInternalName, clm.FriendlyName, clm.ClaimTypeName, true));

              }

       }

}

catch (Exception ex)

{

       Debug.WriteLine("Error filling hierarchy: " + ex.Message);

}

 

이 코드에서는 지원 대상 클레임 유형 목록을 가져왔는지를 확인합니다. 가져오지 않은 경우 CASI KIT 사용자 지정 컨트롤 인스턴스를 만든 다음 WCF를 호출하여 클레임 유형을 검색합니다. 이를 위해 WCF 클래스에 대해 GetClaimTypes 메서드를 호출합니다. 데이터를 가져왔으면 이전에 설명한 AzureClaimTypes라는 클래스 수준 변수에 연결하고 지원 대상 클레임 유형 계층 구조에 추가합니다.

다음으로는 FillResolve 메서드에 대해 살펴보겠습니다. FillResolve 메서드는 두 가지 작업을 수행하므로 서로 다른 두 개의 서명을 포함합니다. 그 중 한 시나리오는 값과 유형이 지정된 특정 클레임이 있고 SharePoint에서 해당 클레임이 올바른지를 확인하는 경우입니다. 두 번째 시나리오는 사용자가 SharePoint 입력 컨트롤에 값을 입력한 경우이므로 사실상 클레임을 검색하는 것과 동일합니다. 이러한 이유로 여기서는 두 작업을 따로 살펴봅니다.

특정 클레임이 있으며 SharePoint에서 해당 값을 확인하려는 경우 GetResolveResults라는 사용자 지정 메서드를 작성한 다음 호출합니다.&nbsp이 메서드에서는 요청을 수행 중인 URI와 SharePoint에서 유효성을 검사하려는 클레임 유형 및 클레임 값을 전달합니다. 이와 같이 작성된 GetResolveResults는 다음과 같습니다.

//Note that claimType is being passed in here for future extensibility; in the

//current case though, we're only using identity claims

private AzureClaimProvider.AzureClaims.UniqueClaimValue GetResolveResults(string siteUrl,

       string searchPattern, string claimType)

{

       AzureClaimProvider.AzureClaims.UniqueClaimValue result = null;

 

       try

       {

              //create an instance of the control

              AzureClaimProvider.DataSource cfgCtrl = new AzureClaimProvider.DataSource();

 

              //set the properties to retrieve data; must configure cache properties since we're using it programmatically

              //cache is not actually used in this case though

              cfgCtrl.WcfUrl = SVC_URI;

              cfgCtrl.OutputType = AzureConnect.WcfConfig.DataOutputType.None;

              cfgCtrl.MethodName = "ResolveClaim";

              cfgCtrl.ServerCacheTime = 10;

              cfgCtrl.ServerCacheName = claimType + ";" + searchPattern;

              cfgCtrl.MethodParams = IDENTITY_CLAIM + ";" + searchPattern;

              cfgCtrl.SharePointClaimsSiteUrl = siteUrl;

 

              //execute the method

              bool success = cfgCtrl.ExecuteRequest();

 

              //if the query encountered no errors then capture the result

              if (success)

                     result = (AzureClaimProvider.AzureClaims.UniqueClaimValue)cfgCtrl.QueryResultsObject;

       }

       catch (Exception ex)

       {

              Debug.WriteLine(ex.Message);

       }

 

       return result;

}

 

보시다시피 여기서는 사용자 지정 CASI Kit 컨트롤 인스턴스를 만든 다음 WCF에 대해 ResolveClaim 메서드를 호출합니다. 이 메서드는 두 개의 매개 변수를 사용하므로 매개 변수를 세미콜론으로 구분된 값으로 전달합니다(CASI Kit에서는 각 매개 변수 값을 세미콜론으로 구분함). 그런 후에 요청을 실행하면 일치하는 항목이 발견되는 경우 UniqueClaimValue 하나가 반환되고 그렇지 않은 경우에는 값으로 null이 반환됩니다. 앞에서 설명한 FillResolve 메서드의 코드는 다음과 같습니다.

protected override void FillResolve(Uri context, string[] entityTypes, SPClaim resolveInput, List<PickerEntity> resolved)

{

       //make sure picker is asking for the type of entity we return; site collection admin won't for example

       if (!EntityTypesContain(entityTypes, SPClaimEntityTypes.User))

              return;

 

       try

       {

              //look for matching claims

              AzureClaimProvider.AzureClaims.UniqueClaimValue result =

                     GetResolveResults(context.AbsoluteUri, resolveInput.Value,

                     resolveInput.ClaimType);

 

              //if we found a match then add it to the resolved list

              if (result != null)

              {

                     PickerEntity pe = GetPickerEntity(result.ClaimValue, result.ClaimType,

                     SPClaimEntityTypes.User, result.DisplayName);

                           resolved.Add(pe);

              }

       }

       catch (Exception ex)

       {

              Debug.WriteLine(ex.Message);

       }

}

 

즉, 요청이 공급자에서 반환하는 유일한 클레임 유형인 사용자 클레임에 대한 요청인지를 먼저 확인합니다. 사용자 클레임에 대한 요청이 아닌 경우 작업을 중지합니다. 다음으로, 메서드를 호출하여 클레임을 확인한 결과가 null이 아니면 클레임을 처리합니다. 처리를 위해 GetPickerEntity라는 또 다른 사용자 지정 메서드를 작성하여 호출합니다. 여기서는 클레임 유형과 값을 전달하여 ID 클레임을 만들며, 그러면 반환되는 PickerEntity를 메서드로 전달된 PickerEntity 목록에 추가할 수 있습니다. 게시물이 너무 길어졌고 블로그의 다른 게시물에서도 관련 설명을 확인할 수 있으므로, 이 게시물에서는 GetPickerEntity 메서드에 대해 별도로 설명하지 않습니다.

다음으로는 두 번째 FillResolve 메서드를 살펴보겠습니다. 앞에서 설명한 것처럼 이 메서드는 기본적으로 검색처럼 작동하므로, 여기서는 대부분의 경우 FillResolve 메서드와 FillSearch 메서드를 결합하여 사용합니다. 이 두 메서드는 모두 다음과 같이 작성된 SearchClaims라는 사용자 지정 메서드를 호출합니다.

private AzureClaimProvider.AzureClaims.UniqueClaimValueCollection SearchClaims(string claimType, string searchPattern,

       string siteUrl)

{

                    

       AzureClaimProvider.AzureClaims.UniqueClaimValueCollection results =

              new AzureClaimProvider.AzureClaims.UniqueClaimValueCollection();

 

       try

       {

              //create an instance of the control

              AzureClaimProvider.DataSource cfgCtrl = new AzureClaimProvider.DataSource();

 

              //set the properties to retrieve data; must configure cache properties since we're using it programmatically

              //cache is not actually used in this case though

              cfgCtrl.WcfUrl = SVC_URI;

              cfgCtrl.OutputType = AzureConnect.WcfConfig.DataOutputType.None;

              cfgCtrl.MethodName = "SearchClaims";

              cfgCtrl.ServerCacheTime = 10;

              cfgCtrl.ServerCacheName = claimType + ";" + searchPattern;

              cfgCtrl.MethodParams = claimType + ";" + searchPattern + ";200";

              cfgCtrl.SharePointClaimsSiteUrl = siteUrl;

 

              //execute the method

              bool success = cfgCtrl.ExecuteRequest();

 

              if (success)

              {

                     //if it worked, get the array of results

                     results =

 (AzureClaimProvider.AzureClaims.UniqueClaimValueCollection)cfgCtrl.QueryResultsObject;

              }

       }

       catch (Exception ex)

       {

              Debug.WriteLine("Error searching claims: " + ex.Message);

       }

 

       return results;

}

 

이 게시물의 다른 부분에서도 확인할 수 있듯이, 이 메서드에서는 사용자 지정 CASI Kit 컨트롤 인스턴스만 만듭니다. WCF에 대해 SearchClaims 메서드를 호출한 다음 검색할 클레임 유형, 해당 클레임 유형에서 찾으려는 클레임 값 및 반환할 최대 레코드 수를 전달합니다. 이 시리즈의 2부에서도 설명한 바 있습니다만, SearchClaims는 전달되는 검색 패턴에 대해 BeginsWith만 수행하므로 사용자가 많으면 결과가 200개보다 많이 나오기 쉽습니다. 그러나 사용자 선택에서 표시하는 최대 일치 항목 수는 200개이므로 결과는 200개이면 충분합니다. 사용자가 200개보다 많은 결과를 스크롤하면서 원하는 결과 항목을 찾을 것 같다고요? 그럴 가능성은 거의 없습니다.

다시 UniqueClaimValues 컬렉션으로 돌아와서 사용자 지정 클레임 공급자에서 두 재정의 메서드를 사용하는 방법을 살펴보겠습니다. 먼저, FillResolve 메서드는 다음과 같습니다.

protected override void FillResolve(Uri context, string[] entityTypes, string resolveInput, List<PickerEntity> resolved)

{

       //this version of resolve is just like a search, so we'll treat it like that

       //make sure picker is asking for the type of entity we return; site collection admin won't for example

       if (!EntityTypesContain(entityTypes, SPClaimEntityTypes.User))

              return;

 

       try

       {

              //do the search for matches

              AzureClaimProvider.AzureClaims.UniqueClaimValueCollection results =

                     SearchClaims(IDENTITY_CLAIM, resolveInput, context.AbsoluteUri);

 

              //go through each match and add a picker entity for it

              foreach (AzureClaimProvider.AzureClaims.UniqueClaimValue cv in results.UniqueClaimValues)

              {

                     PickerEntity pe = GetPickerEntity(cv.ClaimValue, cv.ClaimType, SPClaimEntityTypes.User, cv.DisplayName);

                     resolved.Add(pe);

              }

       }

       catch (Exception ex)

       {

              Debug.WriteLine(ex.Message);

       }

}

이 메서드는 단순히 SearchClaims 메서드를 호출하며, 반환하는 각 결과(있는 경우)에 대해 새 PickerEntity를 만들어 재정의로 전달되는 PickerEntity 목록에 추가합니다. 그러면 모든 PickerEntity가 SharePoint의 입력 컨트롤에 표시됩니다. FillSearch 메서드는 PickerEntity를 다음과 같이 사용합니다.

protected override void FillSearch(Uri context, string[] entityTypes, string searchPattern, string hierarchyNodeID, int maxCount, SPProviderHierarchyTree searchTree)

{

       //make sure picker is asking for the type of entity we return; site collection admin won't for example

       if (!EntityTypesContain(entityTypes, SPClaimEntityTypes.User))

              return;

 

       try

       {

              //do the search for matches

              AzureClaimProvider.AzureClaims.UniqueClaimValueCollection results =

                     SearchClaims(IDENTITY_CLAIM, searchPattern, context.AbsoluteUri);

 

              //if there was more than zero results, add them to the picker

              if (results.UniqueClaimValues.Count() > 0)

              {

                     foreach (AzureClaimProvider.AzureClaims.UniqueClaimValue cv in results.UniqueClaimValues)

                     {

                           //node where we'll stick our matches

                           Microsoft.SharePoint.WebControls.SPProviderHierarchyNode matchNode = null;

 

                           //get a picker entity to add to the dialog

                           PickerEntity pe = GetPickerEntity(cv.ClaimValue, cv.ClaimType, SPClaimEntityTypes.User, cv.DisplayName);

 

                           //add the node where it should be displayed too

                           if (!searchTree.HasChild(cv.ClaimType))

                           {

                                  //create the node so we can show our match in there too

                                  matchNode = new

                                         SPProviderHierarchyNode(ProviderInternalName,

                                         cv.DisplayName, cv.ClaimType, true);

 

                                  //add it to the tree

                                  searchTree.AddChild(matchNode);

                           }

                           else

                                  //get the node for this team

                                  matchNode = searchTree.Children.Where(theNode =>

                                         theNode.HierarchyNodeID == cv.ClaimType).First();

 

                           //add the match to our node

                           matchNode.AddEntity(pe);

                     }

              }

       }

       catch (Exception ex)

       {

              Debug.WriteLine(ex.Message);

       }

}

 

FillSearch에서는 SearchClaims 메서드를 다시 호출합니다. 그리고 반환되는 각 UniqueClaimValue(있는 경우)에 대해 클레임 유형이 결과 계층 구조 노드에 추가되었는지를 확인합니다. 여기서는 항상 하나의 클레임 유형(전자 메일)만 반환되지만, 나중에 더 많은 클레임 유형을 사용할 수 있도록 확장 가능한 방식으로 코드를 작성했습니다. 계층 구조 노드가 없는 경우 추가하고 있는 경우에는 찾습니다. 그런 다음 UniqueClaimsValue에서 작성한 PickerEntity를 가져와 계층 구조 노드에 추가합니다. 이제 코드가 거의 완성되었습니다.

이 게시물에서는 모든 사용자 지정 클레임 공급자가 포함해야 하는 4개 부울 속성 재정의 또는 FillSchema 메서드에 대해서는 다루지 않습니다. 여기서 설명하는 시나리오에서는 이러한 항목을 특별히 사용할 필요가 없으며, 이 블로그의 다른 게시물에서 이러한 항목에 대한 기본 사항을 다루고 있기 때문입니다. 또한 이 사용자 지정 클레임 공급자를 등록하는 데 사용되는 기능 수신기에 대해서도 다루지 않습니다. 마찬가지로 이 프로젝트에서는 기능 수신기를 특별히 사용할 필요가 없으며 다른 게시물에서 기능 수신기에 대해 다루었기 때문입니다. 프로젝트를 컴파일한 후에는 사용자 지정 클레임 공급자 및 사용자 지정 CASI Kit 구성 요소의 어셈블리가 팜의 각 서버에서 전역 어셈블리 캐시에 등록되어 있는지만 확인하면 되며, 이 블로그의 다른 부분에서 설명한 것처럼 SPTrustedIdentityTokenIssuer가 사용자 지정 클레임 공급자를 기본 공급자로 사용하도록 구성해야 합니다. 

지금까지 기본 시나리오를 전반적으로 살펴보았습니다. SharePoint 사이트에서 새 사용자(전자 메일 클레임)를 추가하려고 하면 먼저 사용자 지정 클레임 공급자가 호출되어 지원되는 클레임 유형 목록을 가져오며, 입력 컨트롤에 값을 입력하거나 사용자 선택을 통해 값을 검색할 때 클레임 유형 목록을 다시 가져옵니다. 각각의 경우에서 사용자 지정 클레임 공급자는 사용자 지정 CASI Kit 컨트롤을 사용하여 Windows Azure에 대해 인증된 호출을 수행함으로써 WCF와 통신하며, WCF는 사용자 지정 데이터 클래스를 사용하여 Azure 테이블 저장소에서 데이터를 검색합니다. WCF가 반환하는 결과를 사용자에게 제공하면 됩니다. 이러한 방식으로 완전한 통합형 SharePoint/Azure 솔루션을 작성한 다음 그대로 사용해도 되고 용도에 맞게 수정해도 됩니다. 사용자 지정 CASI Kit 구성 요소, Azure 큐에 사용자를 등록하는 웹 파트, 그리고 사용자 지정 클레임 공급자의 소스 코드는 모두 이 게시물에 첨부되어 있습니다. 소스 코드를 유용하게 활용하시고, 개별 서비스를 결합해 문제를 해결해 주는 솔루션을 작성하는 방법을 파악하실 수 있기를 바랍니다. 아래에는 최종 솔루션의 스크린샷 몇 개가 나와 있습니다.

루트 사이트(익명 사용자):

인증 후의 루트 사이트: 여기서는 웹 파트에 회원 가입 요청(Request Membership) 단추가 표시됩니다.

작동 중인 사용자 선택의 예("sp"로 시작하는 클레임 값을 검색한 후):

 

이 문서는 번역된 블로그 게시물입니다. 원본 문서는 The Azure Custom Claim Provider for SharePoint Project Part 3을 참조하십시오.