달력

42024  이전 다음

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30

출처 : http://msdn.microsoft.com/ko-kr/magazine/cc135979.aspx




.NET Framework 3.5의 디렉터리 보안 주체 관리
Joe Kaplan and Ethan Wilansky
코드 다운로드 위치: DSAccountManagement2008_01.exe (160 KB)
Browse the Code Online


 

이 기사는 Visual Studio 2008 시험판 버전을 기준으로 합니다. 여기에 포함된 모든 정보는 변경될 수 있습니다.

이 기사에서 다루는 내용:
  • System.DirectoryServices.AccountManagement 클래스
  • Active Directory 도메인 서비스
  • AD LDS(Active Directory Lightweight Directory Services)
  • 사용자, 컴퓨터 및 그룹 보안 주체 관리
이 기사에서 사용하는 기술:
.NET Framework 3.5, Visual Studio 2008

디렉터리는 엔터프라이즈 응용 프로그램 개발의 중요한 구성 요소이지만 디렉터리에 대해 정통한 개발자는 드뭅니다. Windows® 플랫폼의 경우 Microsoft는 3가지 기본 디렉터리 플랫폼을 제공하고 있습니다. Active Directory® 도메인 서비스, 모든 Windows 컴퓨터의 로컬 SAM(보안 계정 관리자) 데이터 저장소, 그리고 기존에 ADAM(Active Directory Application Mode)이라고 알려진 AD LDS(Active Directory Lightweight Directory Services)가 그것입니다. 엔터프라이즈 개발자라면 대부분 적어도 SQL 프로그래밍의 기본은 알고 있지만 디렉터리 서비스 프로그래밍 경험이 있는 개발자는 많지 않습니다.
Microsoft® .NET Framework의 초기 버전에서는 System.DirectoryServices 네임스페이스에 디렉터리 서비스 프로그래밍을 위한 클래스 집합을 제공했습니다. 이러한 클래스는 기존 COM 기반 OS 구성 요소, 특히 ADSI(Active Directory 서비스 인터페이스)에 대한 관리되는 단순한 interop 계층이었습니다. 프로그래밍 모델은 상당히 강력했지만 완전한 ADSI 모델에 비해 일반화되고 단순화된 형태였습니다.
이어 Microsoft는 .NET Framework 2.0에서 System.DirectoryServices에 기능을 추가하고 System.DirectoryServices.ActiveDirectory와 System.DirectoryServices.Protocols라는 두 가지 새로운 네임스페이스를 제공하기 시작했습니다. 이 기사에서는 두 네임스페이스를 각각 ActiveDirectory 네임스페이스 및 Protocols 네임스페이스라고 하겠습니다. ActiveDirectory 네임스페이스는 서버, 도메인, 포리스트, 스키마, 복제 등 인프라 수준 구성 요소를 강력한 형식으로 관리하는 다양한 새 클래스를 제공합니다. 반면 Protocols 네임스페이스는 LDAP(Lightweight Directory Access Protocol) 프로그래밍을 위한 대체 API를 제공하는 다른 방향을 선택했습니다. 이 네임스페이스는 ADSI COM interop 계층을 전혀 거치지 않고 Windows LDAP 하위 시스템(wldap32.dll)과 직접 작업합니다(그림 1 참조).
그림 1 Microsoft 디렉터리 서비스 프로그래밍 아키텍처 (더 크게 보려면 이미지를 클릭하십시오.)
그러나 개발자들은 여전히 사용자 및 그룹과 같은 이전의 보안 주체를 관리하는 데 사용했던 ADSI의 몇 가지 강력한 형식의 인터페이스를 원했습니다. System.DirectoryServices의 일반적인 클래스를 사용해도 이러한 작업을 대부분 수행할 수 있었지만 사용하기가 다소 까다로웠고 난해한 작업이 많았습니다. 이에 .NET Framework 3.5에는 보안 주체 관리를 위해 특별히 설계된 System.DirectoryServices.AccountManagement라는 새로운 네임스페이스가 추가되었습니다. 이 기사에서는 System.DirectoryServices.AccountManagement를 간단히 AccountManagement 네임스페이스로 지칭하도록 하겠습니다.
이 새로운 네임스페이스에는 3가지 주요 목표가 있습니다. 3가지 디렉터리 플랫폼 전반에 걸친 보안 주체 관리 작업을 단순화하고, 기본 디렉터리에 관계없이 보안 주체 관리 작업이 일관되도록 하며, 주의 사항이나 특수한 상황을 모두 기억하지 않고도 안정적인 작업 결과를 얻을 수 있도록 하는 것이 바로 이러한 목표입니다.
.NET이 자리 잡기까지 수년을 기다린 끝에 Microsoft는 마침내 ADSI에서 제공되던 이전의 방식을 능가하는 솔루션을 내놓을 수 있었습니다. 이는 .NET 기능을 이용하여 동일한 기능을 구현하는 향상된 API를 제공하면서 AD LDS와 같은 새로운 디렉터리 플랫폼도 효과적으로 지원함에 따라 가능했습니다.

디렉터리 서비스 프로그래밍 아키텍처
본론으로 들어가기 전에 이 기사의 코드 다운로드에서 필수 구성 요소를 확인하십시오. 여기서 설명하는 기법을 사용하기 위해서는 이러한 항목이 필요합니다. 준비가 되었으면 본격적으로 시작해 보도록 하겠습니다. 그림 1은 System.DirectoryServices의 전반적인 프로그래밍 아키텍처를 보여 줍니다. AccountManagement 네임스페이스는 ActiveDirectory 네임스페이스와 마찬가지로 System.DirectoryServices 위에 있는 추상화 계층입니다. 그리고 System.DirectoryServices 자체도 ADSI 위에 있는 추상화 계층입니다. 또한 AccountManagement 네임스페이스는 고성능 인증과 같은 몇 가지 기능을 제공하기 위해 Protocols 네임스페이스를 사용합니다. 그림 1의 남색 음영은 디렉터리 서비스 프로그래밍 아키텍처에서 AccountManagement가 사용하는 부분을 나타냅니다.
그림 2에는 AccountManagement 네임스페이스의 핵심 형식이 나와 있습니다. .NET Framework 2.0에 추가된 네임스페이스와는 달리 AccountManagement는 노출 영역이 상대적으로 작습니다. 열거 및 예외 클래스와 같은 몇 가지 지원 형식을 제외하면 AccountManagement는 크게 강력한 형식의 사용자, 그룹 및 컴퓨터 개체를 나타내는 보안 주체 파생 클래스의 트리, 기본 저장소 연결에 사용되는 PrincipalContext 클래스, 디렉터리에서 개체를 찾는 데 사용되는 PrincipalSearcher 클래스(지원 형식과 함께 제공)의 3가지 구성 요소로 이루어져 있습니다.
그림 2 System.DirectoryServices.AccountManagement의 핵심 클래스 (더 크게 보려면 이미지를 클릭하십시오.)
내부적으로 AccountManagement 네임스페이스는 지원되는 3가지 디렉터리 플랫폼에서 작동할 수 있도록 공급자 디자인 패턴을 사용합니다. 결과적으로 기본 디렉터리 저장소에 관계없이 다양한 Principal 클래스의 멤버가 비슷하게 작동합니다. 이는 단순성과 일관성을 유지하는 데 핵심이 되는 설계입니다.

컨텍스트 설정
대상 디렉터리에 연결하고 디렉터리 작업을 수행하는 데 필요한 자격 증명을 지정하는 데는 PrincipalContext 클래스를 사용합니다. 이러한 방식은 ActiveDirectory 네임스페이스에서 DirectoryContext 클래스와의 컨텍스트를 설정하는 방법과 비슷합니다.
PrincipalContext 생성자에는 컨텍스트 설정에 정확히 개발자가 원하는 대로 옵션을 제공하기 위한 다양한 오버로드가 있습니다. System.DirectoryServices의 DirectoryEntry 클래스를 작업에 사용하다 보면 많은 PrincipalContext 옵션이 익숙하게 느껴질 것입니다. 그러나 ContextType, name 및 container의 3가지 PrincipalContext 옵션은 DirectoryEntry 클래스에 사용하는 입력 매개 변수보다 훨씬 한정적입니다. 이렇게 옵션의 용도를 구체화함으로써 항상 올바른 입력 매개 변수 조합을 사용하도록 한 것입니다. System.DirectoryServices 및 ADSI에서 PrincipalContext 생성자의 이 3가지 매개 변수는 경로라는 단일 문자열로 조합됩니다. 이러한 구성 요소를 분리함으로써 경로의 각 부분이 어떠한 기능을 하는지를 쉽게 이해할 수 있습니다.
ContextType 열거형을 사용하여 대상 디렉터리 유형을 Domain(Active Directory 도메인 서비스의 경우), ApplicationDirectory(AD LDS의 경우) 또는 Machine(로컬 SAM 데이터베이스의 경우)으로 지정합니다. 반면 System.DirectoryServices를 사용할 때는 경로 문자열의 공급자 구성 요소(일반적으로 "LDAP" 또는 "WinNT")를 사용하여 대상 저장소를 지정합니다. 그러면 ADSI가 내부적으로 이 값을 읽어 적절한 공급자를 로드합니다.
AccountManagement 네임스페이스의 클래스는 프레임워크에서 지원되는 공급자만 사용하도록 하여 이 작업을 쉽고 일관성 있게 만들 뿐 아니라, ADSI 및 System.DirectoryServices 프로그래밍에서 자주 발생하는 공급자 입력 실수나 잘못된 대/소문자 사용에 따른 성가신 문제도 방지합니다. 바꾸어 말하면 AccountManagement는 IIS 및 Novell Directory Services와 같이 일반적으로 잘 사용되지 않는 ADSI 공급자는 지원하지 않습니다.
연결할 특정 디렉터리의 이름을 제공하려면 PrincipalContext 생성자에 name 매개 변수를 사용해야 합니다. 이 매개 변수의 값은 특정 서버, 시스템 또는 도메인의 이름이 될 수 있습니다. 이 매개 변수가 null이면 AccountManagement가 현재 보안 컨텍스트를 기준으로 연결을 위한 기본 시스템 또는 도메인을 결정합니다. 그러나 AD LDS 저장소에 연결하려면 name 매개 변수의 값을 지정해야 합니다.
container 매개 변수는 컨텍스트를 설정할 디렉터리의 대상 위치를 지정하는 데 사용됩니다. SAM 데이터베이스는 계층 구조가 아니므로 Machine ContextType을 사용할 때는 이 매개 변수를 지정하지 마십시오. 반대로 AD LDS는 디렉터리 루트 개체를 유추할 때 사용할 defaultNamingContext 특성을 게시하지 않기 때문에 ApplicationDirectory를 사용할 때는 반드시 이 매개 변수에 값을 제공해야 합니다. Domain ContextType의 경우 이 매개 변수를 선택적으로 지정할 수 있으며 지정하지 않으면 AccountManagement는 defaultNamingContext를 사용합니다.
필요에 따라 추가 매개 변수(username, password 및 ContextOptions 열거형)를 사용하여 일반 텍스트 자격 증명을 제공하고 사용할 여러 가지 연결 보안 옵션을 지정할 수 있습니다.
모든 디렉터리는 Windows 협상 인증 방법을 지원합니다. Machine 저장소에 대해 옵션을 지정하지 않으면 Windows 협상 인증이 사용됩니다. 그러나 Domain 및 ApplicationDirectory의 경우에는 서명 및 밀봉을 모두 포함한 Windows 협상이 기본으로 사용됩니다.
Active Directory 도메인 서비스와 AD LDS는 LDAP 단순 바인딩도 지원합니다. 일반적으로 Active Directory 도메인 서비스에는 LDAP 단순 바인딩을 사용하지 않겠지만 AD LDS의 경우 AD LDS 저장소의 사용자가 보안 주체 작업을 수행하도록 하려면 LDAP 단순 바인딩이 필요할 수 있습니다.
username 또는 password 매개 변수를 null로 지정하면 AccountManagement가 현재 Windows 보안 컨텍스트를 사용합니다. 자격 증명을 지정할 때 지원되는 사용자 이름 형식으로는 SamAccountName, UserPrincipalName 및 NT4Name이 있습니다. 그림 3은 컨텍스트를 설정하는 3가지 방법을 보여 줍니다.
// TechWriters OU를 가르키고 기본 자격 증명을 사용하는
// Fabrikam이라는 도메인에 대한 컨텍스트 만들기
PrincipalContext domainContext = new PrincipalContext( 
  ContextType.Domain,"Fabrikam","ou=TechWriters,dc=fabrikam,dc=com");

// 현재 보안 컨텍스트를 사용하여
// 현재 시스템 SAM 저장소에 대한 컨텍스트 만들기
PrincipalContext machineContext = new PrincipalContext(
  ContextType.Machine);

//AD LDS 저장소의 사용자 자격 증명 및
// SSL 암호화를 사용하여 파티션 루트를 가리키는
// AD LDS 저장소에 대한 컨텍스트 만들기
PrincipalContext ldsContext = new PrincipalContext(
    ContextType.ApplicationDirectory, "sea-dc-02.fabrikam.com:50001", 
    "ou=ADAM Users,o=microsoft,c=us", 
    ContextOptions.SecureSocketLayer | ContextOptions.SimpleBind, 
    "CN=administrator,OU=ADAM Users,O=Microsoft,C=US ", "pass@1w0rd01");

AccountManagement PrincipalContext 및 System.DirectoryServices DirectoryEntry 클래스 사이에는 바인딩 작업 시에 작지만 중요한 차이가 있습니다. PrincipalContext는 개체 생성 시 기본 디렉터리에 연결하고 바인딩하지만 DirectoryEntry는 연결이 필요한 다른 작업을 수행할 때까지 바인딩하지 않습니다. 결과적으로 PrincipalContext를 사용하면 연결이 디렉터리에 성공적으로 바인딩되었는지 바로 알 수 있습니다.

사용자 계정 만들기
이제 AccountManagement가 PrincipalContext를 사용하여 컨테이너에 연결하고 바인딩하는 과정은 충분히 배웠습니다. 다음은 일반적인 DirectoryServices 작업인 사용자 계정 만들기에 대해 알아보겠습니다. 이 과정에서 각 코드 샘플은 하나의 필수 특성에 값을 할당하고, 두 개의 선택적인 특성을 추가하며, 암호를 설정하고, 사용자 계정을 활성화한 다음, 변경 내용을 디렉터리로 커밋합니다.
여기에서는 그림 3의 예 1에서 소개한 domainContext 변수를 사용하여 새 UserPrincipal을 만듭니다.
// 사용자 보안 주체 개체 만들기
UserPrincipal user = new UserPrincipal(domainContext,
     "User1Acct", "pass@1w0rd01", true);

// 사용자 보안 주체에 일부 속성 할당
user.GivenName = "User";
user.Surname = "One";

// 다음 로그인할 때 암호를 변경하도록 함
user.ExpirePasswordNow();

// 사용자를 디렉터리에 저장
user.Save();
(참고: 프로그래머 주석은 예제 프로그램 파일에는 영문으로 제공되며 기사에는 이해를 돕기 위해 번역문으로 제공됩니다.)
domainContext는 작업 수행에 사용되는 디렉터리에 대한 연결과 보안 컨텍스트를 설정합니다. 그리고 한 줄의 코드로 새 user 개체를 만들고 암호를 설정하여 활성화합니다. 그런 다음 GivenName 및 Surname 속성을 사용하여 기본 저장소의 해당 디렉터리 특성을 설정합니다. 기본 디렉터리 저장소에 개체를 저장하기 전에 암호가 만료되어 사용자가 첫 번째 로그온 시에 암호를 변경하도록 합니다.
그림 4에는 이에 해당하는 System.DirectoryServices에서 사용자 계정을 만드는 단계가 나와 있습니다. 첫 번째 코드 줄의 container 변수는 연결에 경로를 사용하는 DirectoryEntry 클래스 개체입니다. 이 경로는 공급자, 도메인 및 컨테이너(TechWriters OU)를 지정하고 현재 사용자의 보안 컨텍스트를 사용하여 연결합니다. container 변수는 사용자 보안 주체를 만드는 이전 예에서 domainContext와 비슷합니다.
    DirectoryEntry container =
    new DirectoryEntry("LDAP://ou=TechWriters,dc=fabrikam,dc=com");
// 컨테이너에 사용자 디렉터리 엔트리 만들기
DirectoryEntry newUser = container.Children.Add("cn=user1Acct", "user");

// samAccountName 필수 특성 추가
newUser.Properties["sAMAccountName"].Value = "User1Acct";

// 선택적 특성 추가
newUser.Properties["givenName"].Value = "User";
newUser.Properties["sn"].Value = "One";

// 디렉터리에 저장
newUser.CommitChanges();

// Invoke 메서드 및 IadsUser.SetPassword를 사용하여
// 사용자 계정에 대한 암호 설정
newUser.Invoke("SetPassword", new object[] { "pAssw0rdO1" });

// 다음 로그인할 때 암호를 변경하도록 지정
newUser.Properties["pwdLastSet"].Value = 0;

// 사용자 계정을 활성화
// newUser.InvokeSet("AccountDisabled", new object[]{false});
// 또는 ADS_UF_NORMAL_ACCOUNT (512)를 사용하여 효과적으로
// 비활성화된 비트를 설정 해제
newUser.Properties["userAccountControl"].Value = 512;

// 디렉터리에 저장
newUser.CommitChanges();
   
이 코드는 AccountManagement 예보다 훨씬 길고 복잡합니다. 따라서 디렉터리의 기본 데이터 모델에 대해 더 많이 알아야 하고, 초기 암호를 설정하거나 disabled 플래그를 해제하여 user 개체를 활성화하는 등, 특정 계정 관리 기능을 처리하는 방법도 알아야 합니다. 이는 리플렉션 기반 Invoke 및 InvokeSet 메서드를 사용하여 기본 ADSI COM 인터페이스 멤버를 호출해야 하는 경우 까다로울 수 있습니다. 많은 개발자가 디렉터리 서비스 프로그래밍을 어렵게 생각하는 이유도 바로 이 때문입니다.
AD LDS 또는 로컬 SAM 데이터베이스에서 같은 사용자 계정을 만들 때는 차이가 더 커집니다. AD LDS와 Active Directory 도메인 서비스는 사용자 계정을 활성화하는 데 서로 다른 메커니즘(Active Directory 도메인 서비스는 userAccountControl 특성을 사용하지만 AD LDS는 msds-userAccountDisabled 특성을 사용)을 사용하며, SAM 저장소의 경우 ADSI 인터페이스 멤버를 호출해야 합니다. 이렇게 3가지 디렉터리 저장소의 계정 관리 기능이 서로 다르다는 점은 AccountManagement 네임스페이스 설계에 있어서 해결되어야 할 중요한 과제였습니다. 그러나 이제 UserPrincipal 개체를 만드는 데 사용되는 PrincipalContext를 변경함으로써 간단히 디렉터리 저장소 사이를 전환하고 일관성 있는 한 가지 계정 관리 기능 집합을 얻을 수 있게 되었습니다.
.NET Framework 2.0에 도입된 Protocols 네임스페이스는 이러한 문제 해결과는 전혀 관련이 없었습니다. 단지 시스템 수준 LDAP 프로그래머에게 LDAP 기반 응용 프로그램 작성에 사용할 수 있는 보다 강력하고 유연한 API를 제공하기 위한 것이었습니다. System.DirectoryServices보다 LDAP 모델에 대한 추상화 수준이 낮기 때문에 다양한 디렉터리 간의 차이를 줄이는 데는 효과가 없었습니다. 게다가 LDAP 디렉터리가 아닌 로컬 SAM 데이터베이스에는 사용할 수 없었습니다. 이 기사와 함께 제공되는 온라인 코드 샘플에는 그림 3 다음에 나오는 AccountManager 예와 비슷한 샘플이 포함되어 있습니다. 이 샘플은 해당 예와 동일한 작업을 수행하지만 코드 줄 수는 3배에 달합니다.
그림 5의 PrincipalContext는 SAM 저장소를 대상으로 지정하기 위해 시스템을 참조하는 ContextType 열거형을 보여 줍니다. 다음 매개 변수는 실제 시스템을 이름 또는 IP 주소를 기준으로 대상으로 지정하고 마지막 두 값은 계정 생성을 수행할 수 있는 계정의 자격 증명을 제공합니다.
PrincipalContext principalContext = new PrincipalContext(
    ContextType.Machine. "computer01", "adminUser", "adminPassword");
   
UserPrincipal user = new UserPrincipal(principalContext,
    "User1Acct", "pass@1w0rd01", true);

//다른 저장소에 액세스할 때는 특성에 차이가 있으며
//IntelliSense에 나타나는 특성은 기반 저장소에서
//파생되지 않음을 유의하십시오.
user.Name = "User One";
user.Description = "User One";
user.ExpirePasswordNow();
user.Save();
Active Directory 도메인 서비스와는 달리 SAM 저장소에는 givenName, sn 또는 displayName 특성이 없기 때문에 Name 및 Description 속성을 설정해야 합니다. AccountManagement는 3가지 디렉터리 저장소 모두에 일관성 있는 환경을 제공하기는 하지만 기본 모델 자체에 차이가 있습니다. 그러나 기본 저장소에서 제공되지 않는 특성을 가져오려고 하면 InvalidOperationException이 발생합니다.
그림 3과 그림 5는 사용자 계정 생성 방법을 보여 주는 두 가지 예입니다. 이 예에서는 저장소에 관계없이 작업을 수행하는 AccountManagement 네임스페이스의 일관성 있는 고유 프로그래밍 모델을 보여 줍니다. 그림 6의 PrincipalContext는 AD LDS 저장소를 대상으로 지정하기 위해 그림 3과 같이 ContextType.ApplicationDirectory를 사용합니다. 다음 매개 변수는 AD LDS 서버를 보여 줍니다. 이 예에서 sea-dc-02.fabrikam.com은 AD LDS 인스턴스를 호스트하는 서버의 FQDN(정규화된 도메인 이름)이며 인스턴스는 50001 포트에서 SSL 통신을 수신합니다. 코드 다운로드에서는 이와 달리 50000 포트를 통한 비 SSL 통신을 사용합니다. 안전하지는 않지만 테스트 용도로는 문제가 없을 것입니다.
PrincipalContext principalContext = new PrincipalContext(
    ContextType.ApplicationDirectory,
    "sea-dc-02.fabrikam.com:50001",
    "ou=ADAM Users,o=microsoft,c=us",
    ContextOptions.SecureSocketLayer | ContextOptions.SimpleBind,
    "CN=administrator,OU=ADAM Users,O=Microsoft,C=US",
    "P@55w0rd0987");

UserPrincipal user = new UserPrincipal(principalContext,
    "User1Acct", "pass@1w0rd01", true);

user.GivenName = "User";
user.Surname = "One";

user.Save();

다음 매개 변수는 CRUD(Create/Read/Update/Delete) 작업을 수행할 컨테이너를 지정합니다. 이 예에서는 CRUD 작업 수행을 위해 AD LDS에 저장된 사용자를 지정하므로 LDAP 단순 바인딩을 사용해야 하며 이는 보안을 위해 SSL과 결합됩니다. AD LDS는 기본적으로 보안 DIGEST 인증을 지원하지만 ADSI는 이를 지원하지 않습니다. 이번에도 역시 이 예는 PrincipalContext가 크게 다르지만 이전의 두 예와 사실상 동일합니다.
AccountManagement 네임스페이스는 암호 만료, 계정 잠금 해제와 같은 포괄적인 계정 관리 기능 집합을 제공합니다. 이 기사에서 이러한 기능을 모두 소개할 수는 없지만 이러한 기능은 디렉터리 저장소 유형에 관계없이 일관적이고 안정적으로 작동하며 이를 구현하기 위한 복잡한 과정이 없어졌습니다.

그룹 및 컴퓨터 만들기
사용자 계정을 만드는 작업은 DirectoryServices 저장소에 관계없이 간단하고 일관성 있게 수행된다는 것을 살펴보았습니다. 이러한 일관성은 지원되는 다른 두 가지 DirectoryServices 개체인 group 및 computer를 만드는 데도 마찬가지로 유지됩니다. UserPrincipal 클래스와 마찬가지로 GroupPrincipal 및 ComputerPrincipal 클래스도 Principal 추상 클래스에서 상속하고 비슷하게 작동합니다. 예를 들어 다음 코드를 사용하면 Active Directory 도메인 서비스, AD LDS 또는 SAM 계정 데이터베이스에 Group01이라는 그룹을 만들 수 있습니다.
GroupPrincipal group = new GroupPrincipal(principalContext,
    "Group01");
group.Save();
각 경우의 차이점은 서로 다른 저장소와의 컨텍스트를 설정하는 PrincipalContext 클래스에 포함됩니다. computer 개체를 생성하는 데 사용되는 코드에서도 컨텍스트를 사용하여 principal 개체를 생성한 다음 보안 주체 컨텍스트의 대상에 개체를 저장하는 비슷한 패턴을 따릅니다.
ComputerPrincipal computer = new ComputerPrincipal(domainContext);
computer.DisplayName = "Computer1";
computer.SamAccountName = "Computer1$";
computer.Enabled = true;
computer.SetPassword("p@ssw0rd01");
computer.Save();
이번에도 AccountManagement는 지원되는 모든 ID 저장소의 상호 작용 모델을 일관성 있게 유지합니다. 이 샘플은 도메인에 가입할 수 있는 computer 개체(sAMAccountName 특성 끝에 $ 필요)를 만드는 구문으로도 사용할 수 있지만 여기에서는 표시 이름 및 일반 이름을 설정하므로 $를 포함하지 않습니다. SAM 데이터베이스와 AD LDS에는 computer 클래스가 없기 때문에 AccountManagement는 도메인 기반 PrincipalContext 내에서만 이러한 유형의 개체를 만들도록 허용합니다. 또한 Active Directory 도메인 서비스에만 글로벌, 유니버설 및 도메인 로컬의 다양한 그룹 범위가 있으며 보안 및 메일 그룹이 모두 포함되어 있습니다. 따라서 GroupPrincipal 클래스는 필요에 따라 이러한 값을 설정할 수 있도록 null 허용 속성을 제공합니다.

그룹 멤버 자격 관리
AccountManagement 네임스페이스는 그룹 멤버 자격 관리도 간소화해 줍니다. AccountManagement가 개발되기 전에는 그룹 관리 방식에 있어서 저장소 유형별로 고유한 부분이 많았고 당연히 일관성도 없었습니다. 또한 구성원이 많은 그룹을 프로그래밍 방식으로 관리하기도 어려웠습니다. 게다가 SAM 그룹 멤버 자격을 관리하는 데는 COM interop을 사용하고 Active Directory 도메인 서비스 및 AD LDS 그룹을 관리하는 데는 LDAP 특성을 사용해야 했습니다. 그러나 이제 저장소에 관계없이 GroupPrincipal 클래스의 Members 속성을 사용하여 그룹의 멤버 자격을 열거하고 구성원을 관리할 수 있게 되었습니다.
사용자가 속한 모든 그룹을 가져오는 작업도 간단해 보이지만 실제로는 구현하기가 까다롭습니다. AccountManagement는 이를 위해 사용할 수 있는 몇 가지 메서드를 제공합니다. Principal 기본 클래스에는 GetGroups 메서드 및 IsMemberOf 메서드가 두 개씩 포함되어 있습니다. 이 메서드는 Principal 형식의 그룹 멤버 자격을 가져오고 해당 보안 주체가 그룹의 구성원인지 확인합니다. 또한 UserPrincipal은 모든 UserPrincipal 형식의 완전히 확장된 보안 그룹 멤버 자격을 반환하는 특수한 GetAuthorizationGroups 메서드도 제공합니다. 그림 7은 GetAuthorizationGroups 메서드 사용 방법을 보여 줍니다.
string userName = "user1Acct";

// ID 저장소에서 사용자 찾기
UserPrincipal user =
    UserPrincipal.FindByIdentity(
        adPrincipalContext, 
        userName);

// 사용자 보안 주체에 대한 그룹을 얻고
// 결과를 PrincipalSearchResult object에 저장
PrincipalSearchResult<Principal> results = 
    user.GetAuthorizationGroups();

// 사용자가 속할 그룹의 이름을 표시
Console.WriteLine("groups to which {0} belongs:", userName);
foreach (Principal result in results)
{
    Console.WriteLine("name: {0}", result.Name);
}

트러스트된 도메인 또는 외부 보안 주체 간에 그룹 멤버 자격을 확장하는 작업도 AccountManagement에 의해 간소화되었습니다. 이 작업은 Principal 클래스의 GetGroups(PrincipalContext) 메서드가 담당합니다.

자체 찾기
디렉터리에서 개체를 찾는 것도 프로그래머가 어려움을 겪는 작업 중 하나입니다. 개발자가 일상적으로 다루는 구문에 비해 LDAP가 특별히 복잡한 쿼리 언어라고 할 수는 없지만 생소한 것이 사실입니다. LDAP에 대해 기초 지식이 있더라도 이를 사용하여 일반적인 작업을 수행하는 방법을 알아내기는 쉽지 않습니다.
이번에도 AccountManagement는 개체를 찾는 작업을 간단하게 처리할 수 있도록 FindByIdentity 메서드를 제공합니다. 이 메서드는 UserPrincipal, GroupPrincipal 및 ComputerPrincipal 클래스가 상속하는 Principal 추상 클래스에 포함되어 있습니다. 따라서 이러한 Principal 형식 중 하나를 검색해야 할 때마다 FindByIdentity를 사용하면 됩니다.
FindByIdentity에는 PrincipalContext 및 찾을 값을 받는 두 개의 오버로드가 포함되어 있습니다. 값으로는 SamAccountName, Name, UserPrincipalName, DistinguishedName, Sid 또는 Guid의 지원되는 ID 유형을 사용할 수 있습니다. 두 번째 오버로드의 경우에도 값으로 지정할 ID 유형을 명시적으로 정의할 수 있습니다.
좀 더 간단한 오버로드를 먼저 살펴보면, 이전 예에서 만든 사용자 계정을 FindByIdentity를 사용하여 반환하는 코드는 다음과 같습니다.
UserPrincipal user = UserPrincipal.FindByIdentity(principalContext, "user1Acct");
컨텍스트가 설정되면(principalContext 개체에 저장) FindByIdentity 메서드를 사용하여 principal 개체(이 예의 경우 UserPrincipal)를 검색할 수 있습니다. 지원되는 ID 저장소에 대한 컨텍스트를 설정한 이후에는 ID를 검색하는 코드는 항상 동일합니다.
두 번째 FindByIdentity 생성자는 명시적으로 ID 형식을 지정할 수 있도록 해 줍니다. 이 생성자를 사용할 때는 값의 형식이 지정할 ID 유형과 일치하도록 해야 합니다. 예를 들어 다음 코드는 디렉터리의 지정한 위치에 해당 개체가 있는 경우 고유 이름을 사용하여 UserPrincipal을 반환합니다.
UserPrincipal user = UserPrincipal.FindByIdentity(
    adPrincipalContext, 
   IdentityType.DistinguishedName,
   "CN=User1Acct,OU=TechWriters,DC=FABRIKAM,DC=COM");
반면에 다음 코드는 IdentityType 열거형이 DistinguishedName 형식을 지정하지만 값은 이 형식이 아니므로 UserPrincipal을 반환하지 않습니다.
UserPrincipal user = UserPrincipal.FindByIdentity(
    adPrincipalContext,
   IdentityType.DistinguishedName,
   "user1Acct");
형식은 매우 중요합니다. 예를 들어 GUID 또는 SID IdentityType을 사용할 때는 각각 표준 COM GUID 형식 및 SDDL(Security Descriptor Description Language) 형식의 값을 사용해야 합니다. 이 기사의 코드 다운로드에는 올바른 형식을 보여 주는 두 가지 메서드(FindByIdentityGuid 및 FindByIdentitySid)가 포함되어 있습니다. 사용자의 디렉터리 저장소에서 일치하는 결과를 찾으려면 이러한 메서드의 GUID 또는 SID 값을 변경해야 합니다. 잠시 후 살펴보겠지만 PrincipalSearcher 클래스를 사용하면 이러한 형식을 쉽게 얻을 수 있습니다.
이제 Principal 개체를 찾아 바인딩했으므로 이에 대한 작업을 수행하기는 어렵지 않습니다. 예를 들어 다음과 같이 그룹에 사용자를 추가할 수 있습니다.
// 사용자 보안 주체 얻기
UserPrincipal user = 
    UserPrincipal.FindByIdentity(adPrincipalContext, "User1Acct");
// 그룹 보안 주체 얻기
GroupPrincipal group = 
    GroupPrincipal.FindByIdentity(adPrincipalContext, "Administrators");
// 사용자 추가
group.Members.Add(user);
// 변경 내용을 디렉터리에 저장
group.Save();
여기에서는 사용자를 찾은 다음 그룹을 찾는 데 FindByIdentity 메서드를 사용했습니다. 이러한 보안 주체 개체를 얻은 다음에는 그룹 Members 속성의 Add 메서드를 호출하여 그룹에 사용자 보안 주체를 추가합니다. 마지막으로 그룹의 Save 메서드를 호출하여 변경 내용을 디렉터리에 저장합니다.

일치하는 결과 찾기
강력한 예제에 의한 쿼리(QBE) 기능과 PrincipalSearcher 클래스를 사용하여 정의된 기준에 따라 개체를 찾을 수도 있습니다. QBE와 PrincipalSearcher 클래스에 대해서는 잠시 후에 자세히 설명하기로 하고, 간단한 검색 예를 먼저 살펴보겠습니다. 그림 8은 name/cn 접두사 "user"로 시작하는 비활성화된 사용자 계정을 모두 찾는 방법을 보여 줍니다.
// 검색될 대상을 설명하기 위한 보안 주체 개체 표현 만들기
UserPrincipal user = new UserPrincipal(adPrincipalContext);

// 검색의 속성 정의(와일드카드 사용 가능)
user.Enabled = false;
user.Name = "user*";

// 검색 작업을 실행하기 위한 보안 주체 검색기 만들기
PrincipalSearcher pS = new PrincipalSearcher();

// 만든 보안 주체 개체에 쿼리 필터 속성 할당
// PrincipalSearcher 생성자에서 사용자 보안 주체를 전달할 수도 있음
pS.QueryFilter = user;

// 쿼리 실행
PrincipalSearchResult<Principal> results = pS.FindAll();

Console.WriteLine("Disabled accounts starting with a name of 'user':");
foreach (Principal result in results)
{
    Console.WriteLine("name: {0}", result.Name);
}

PrincipalContext 변수인 adPrincipalContext는 Active Directory 도메인을 가리키지만 AD LDS 응용 프로그램 파티션도 마찬가지로 간단하게 가리킬 수 있습니다. 컨텍스트를 설정한 다음 코드에서는 새로운 UserPrincipal 개체를 만듭니다. 이는 검색 작업 대상이 되는 보안 주체의 메모리 내 표현입니다. 이 보안 주체를 만든 후에는 검색 결과를 제한하는 속성을 설정합니다. 다음 두 줄의 코드는 개발자가 설정할 수 있는 몇 가지 제한을 보여 줍니다. 이 코드는 사용자 이름이 특정 값으로 시작하는 비활성화된 모든 사용자를 지정합니다. Name 특성에 대한 속성 값에는 와일드카드를 사용할 수 있습니다.
LDAP 언어에서 검색 필터를 설정하는 데 익숙한 개발자라면 QBE가 왜 독창적이고 직관적인 대안인지 금방 알 수 있을 것입니다. QBE를 사용하여 쿼리 작업에 사용할 예 개체를 설정합니다. 그림 8에서 생성한 QBE 개체와 동등한 필터를 설정하는 다음 LDAP 언어와 비교해 보면 QBE가 일반적인 DirectoryServices 검색 언어에 비해 간단하다는 사실을 확인할 수 있습니다.
(&(objectCategory=person)(objectClass=user)(name=user*)(userAccount
Control:1.2.840.113556.1.4.803:=2))
이 코드에서 보듯이 LDAP 언어는 훨씬 복잡할 뿐만 아니라 Active Directory LDS 사용자 스키마에는 LDAP 언어에서 사용되는 userAccountControl 특성 대신 msDS-UserAccountDisabled 특성이 사용되므로 AD LDS에 대해서는 작동하지 않습니다. 이러한 차이도 역시 AccountManagement에 의해 내부적으로 처리됩니다.
그림 8에 나와 있는 QBE 개체를 설정하고 나면 PrincipalSearcher 개체를 만들고 코드 이전 부분에서 Principal 개체에 의해 생성된 QueryFilter 속성을 할당합니다. 이렇게 QueryFilter 속성을 설정하는 대신 PrincipalSearcher 생성자에 사용자 보안 주체를 전달할 수도 있습니다. 그런 다음 PrincipalSearcher의 FindAll 메서드를 호출하고 반환되는 결과를 PrincipalSearchResult 일반 목록에 할당하여 쿼리를 실행합니다. PrincipalSearchResult 목록은 반환되는 Principal 개체를 저장합니다. 마지막으로 보안 주체 목록을 열거하고 반환된 각 보안 주체의 Name 특성을 표시합니다.
참조 특성에는 QBE를 사용할 수 없습니다. 즉, QBE 개체에서 소유하지 않는 특성은 개체의 메모리 내 표현을 구성하는 데 사용할 수 없습니다.
foreach 루프를 사용하면 비활성화된 사용자 계정을 활성화하거나 삭제하는 등, 훨씬 다양한 작업이 가능합니다. 읽기 작업만 원하는 경우 다른 ID 저장소를 가리키려면 반환할 특성이 해당 저장소에 있어야 한다는 점에 주의해야 합니다. 예를 들어 AD LDS 사용자에는 sAMAccountName 특성이 없으므로 이 특성을 결과로 반환할 수 없습니다.

까다로운 검색 작업의 단순화
강력한 FindBy 메서드를 PrincipalSearchResult 클래스와 함께 사용하면 다른 방법으로는 검색이 어려운 사용자 및 컴퓨터 보안 주체에 대한 정보를 검색할 수 있습니다. 그림 9는 오늘 암호를 변경한 각 사용자의 이름을 검색하는 방법을 보여 줍니다. 이 예에서는 FindByPasswordSetTime 메서드와 PrincipalSearchResult 클래스를 사용합니다. 기본 pwdLastSet 특성은 디렉터리에 큰 정수로 저장되기 때문에 AccountManagement를 사용하지 않으면 작업이 복잡해집니다.
// 오늘 날짜 얻기
DateTime dt = DateTime.Today;

// 쿼리 실행
PrincipalSearchResult<Principal> results = 
    UserPrincipal.FindByPasswordSetTime(
        adPrincipalContext, 
        dt, 
        MatchType.GreaterThanOrEquals); 

Console.WriteLine("users whose password was set on {0}", 
    dt.ToShortDateString());
foreach (Principal result in results)
{
    Console.WriteLine("name: {0}", result.Name);
}

이 기사의 코드 다운로드에는 다른 FindBy 메서드 사용 예가 포함되어 있습니다. 이러한 다른 예도 모두 작동 방식은 그림 9와 비슷합니다.
FindBy 메서드는 검색하기 어려운 정보를 쉽게 찾는 유용한 도구지만 QBE 기능을 사용하여 결과를 추가로 필터링해야 하는 경우에는 적합하지 않습니다. 관련 특성이 읽기 전용이기 때문에 QBE 개체에서 특성을 설정할 수 없는 것은 물론 QBE가 참조하는 개체의 사용자도 설정할 수 없습니다. QBE를 사용하려면 예 principal 개체에 해당하는 읽기 전용 속성 및 AdvancedSearchFilter 속성을 함께 사용해야 합니다. 이에 대해서는 나중에 설명하도록 하겠습니다. 그림 10에서는 다른 FindBy 메서드를 나열하고 검색에 FindBy 메서드 대신 사용할 수 있는 해당하는 읽기 전용 속성을 보여 줍니다.

메서드 이름 읽기 전용 속성 설명
FindByLogonTime LastLogonTime 지정한 시간 내에 로그온한 계정
FindByExpirationTime AccountExpirationDate 지정한 시간 내에 만료된 계정
FindByPasswordSetTime LastPasswordSetTime 지정한 시간 내에 암호가 설정된 계정
FindByLockoutTime AccountLockoutTime 지정한 시간 내에 잠긴 계정
FindByBadPasswordAttempt LastBadPasswordAttempt 지정한 시간 내에 잘못된 암호가 입력된 계정
해당 메서드 없음 BadLogonCount 지정한 횟수만큼 로그온을 시도했지만 로그온에 실패한 계정
QBE를 구성할 때 읽기 전용 속성의 값은 설정할 수 없습니다. 그렇다면 검색 작업에서 속성은 어떻게 처리해야 할까요? 결과 집합을 열거하는 동안 결과 집합을 검색하고 읽기 전용 속성을 사용하여 조건 테스트를 수행할 수 있습니다. 그러나 크기가 커질 수 있는 결과 집합에는 이러한 방법이 적합하지 않습니다. 읽기 전용 속성으로 필터링되지 않은 결과를 먼저 검색하고 반환된 결과 집합을 읽기 전용 속성으로 필터링해야 하기 때문입니다. 코드 다운로드의 PrincipalSearchEx6v2 메서드는 이러한 비효율적인 방식을 보여 줍니다.
디렉터리 서비스 팀에서는 AuthenticablePrincipal 클래스에 AdvancedSearchFilter 속성을 추가함으로써 QBE의 이러한 한계를 해결했습니다. AdvancedSearchFilter를 사용하면 읽기 전용 속성을 기반으로 검색을 수행한 다음 QBE 메커니즘을 사용하여 설정 가능한 다른 속성과 결합할 수 있습니다. 그림 11은 UserPrincipal 클래스의 LastBadPasswordAttempt 읽기 전용 속성을 사용하여 오늘 잘못된 암호로 액세스를 시도한 사용자를 반환하는 방법을 보여 줍니다.
DateTime dt = DateTime.Today;

// 검색될 대상을 설명하기 위한 보안 주체 개체 표현 만들기
UserPrincipal user = new UserPrincipal(adPrincipalContext);

user.Enabled = true;

// 검색의 속성 정의(와일드카드 사용 가능)
user.Name = "*";

// 쿼리 필터에 LastBadPasswordAttempt >= Today 추가
user.AdvancedSearchFilter.LastBadPasswordAttempt
    (dt, MatchType.GreaterThanOrEquals);

// 검색 작업을 실행하기 위한 보안 주체 검색기를 만들고
// 쿼리 필터로 QBE 사용자 보안 주체를 할당
PrincipalSearcher pS = new PrincipalSearcher(user);

// 쿼리 실행
PrincipalSearchResult<Principal> results = pS.FindAll();

Console.WriteLine("Bad password attempts on {0}:", 
    dt.ToShortDateString());
foreach (UserPrincipal result in results)
{
    Console.WriteLine("name: {0}, {1}",
           result.Name,
           result.LastBadPasswordAttempt.Value);
}


사용자 인증
디렉터리 기반 응용 프로그램을 개발할 때, 특히 AD LDS를 사용하는 경우 디렉터리에 저장된 사용자의 자격 증명을 인증해야 할 때가 많습니다. .NET Framework 3.5가 발표되기 전에 프로그래머들은 System.DirectoryServices의 DirectoryEntry 클래스를 사용하여 내부적으로 강제로 LDAP 바인딩 작업을 수행하여 이를 구현했습니다. 그러나 이러한 코드는 안전성이나 성능이 떨어질 수 있으며 작성하기도 어렵습니다. 또한 ADSI 자체는 이러한 형식의 작업에 맞게 설계되지 않았으며 내부적으로 LDAP 연결을 캐시하는 방식 때문에 사용량이 많은 상황에서는 문제가 발생할 수 있습니다.
이미 설명했듯이 .NET Framework 2.0의 System.DirectoryServices.Protocols 어셈블리에는 연결 기반 프로그래밍 메타포를 사용하는 저수준 LDAP 클래스가 포함되어 있습니다. 이것은 ADSI 고유의 한계를 극복하기 위한 설계이지만 복잡한 코드를 작성해야 한다는 단점이 있습니다.
.NET Framework 3.5의 AccountManagement는 프로그래머가 모든 환경에서 작업할 수 있도록 ASP.NET의 ActiveDirectoryMembershipProvider 구현을 통한 강력한 기능과 사용 편의성을 제공합니다. 또한 AccountManagement 네임스페이스는 필요한 경우 로컬 SAM 데이터베이스에 대해 자격 증명을 인증할 수 있도록 합니다.
PrincipalContext 클래스의 두 ValidateCredentials 메서드를 사용하여 자격 증명 유효성 검사를 수행할 수 있습니다. 먼저 자격 증명의 유효성을 검사할 대상 디렉터리를 사용하여 PrincipalContext의 인스턴스를 만들고 적절한 옵션을 지정합니다. 컨텍스트를 설정한 후 제공한 사용자 이름 및 암호 값에 따라 ValidateCredentials에서 true 또는 false가 반환되는지 테스트합니다. 그림 12는 AD LDS에서 사용자를 인증하는 예를 보여 줍니다.
// AD LDS를 사용하여 컨텍스트 설정
PrincipalContext ldsContext = 
    new PrincipalContext(
        ContextType.ApplicationDirectory, 
        "sea-dc-02.fabrikam.com:50000", 
        "ou=ADAM Users,O=Microsoft,C=US");

// 디렉터리에 유효한 사용자인지 확인
Console.WriteLine(
    ldsContext.ValidateCredentials(
        "user1@adam", 
        "Password1", 
        ContextOptions.SimpleBind + 
        ContextOptions.SecureSocketLayer));

이 방법은 다양한 사용자 자격 증명 집합의 유효성을 빠르고 효율적으로 검사할 때 특히 유용합니다. 해당 디렉터리 저장소에 대한 단일 PrincipalContext 개체를 만들어 ValidateCredentials에 대한 각 호출에 반복적으로 사용할 수 있으며 PrincipalContext가 디렉터리에 대한 연결을 재사용할 수 있어 성능과 확장성 면에 유리합니다. 그리고 ValidateCredentials에 대한 호출은 스레드로부터 안전하므로 여러 스레드에 걸쳐 이 작업을 수행하는 데 인스턴스를 사용할 수 있습니다. PrincipalContext를 만드는 데 사용되는 자격 증명은 ValidateCredentials에 대한 호출로 변경되지 않으며 컨텍스트 및 메서드 호출은 별도의 연결을 유지한다는 점이 중요합니다.
기본적으로 AccountManagement는 보안 Windows 협상 인증을 사용하며 AD LDS에 대해 단순 바인딩을 수행할 때는 SSL 사용을 시도합니다. 따라서 수행할 인증 유형과 사용할 연결 보호 방식(해당하는 경우)을 항상 명시적으로 지정하는 것이 좋지만 기본 설정을 사용하더라도 보안에는 문제가 없습니다.
Windows Server® 2003 이상 버전의 Active Directory 도메인 서비스 및 AD LDS에는 고성능 인증 작업을 위한 빠른 동시 바인딩 기능이 포함되어 있습니다. 이 기능은 사용자에 대한 보안 토큰을 실제로 만들지 않고 사용자 암호의 유효성을 검사합니다. 일반적인 바인딩 작업과는 달리 빠른 동시 바인딩에서는 LDAP 연결 상태가 바인딩되지 않은 채로 유지됩니다. 동일한 연결에 대해 반복적으로 바인딩 작업을 수행하고 간단히 실패한 암호 시도를 확인하는 데 빠른 동시 바인딩을 사용할 수 있습니다. 이 기능은 ADSI 또는 System.DirectoryServices를 통해 사용할 수 있는 옵션이 아니라 Protocols 네임스페이스에서 제공되는 옵션입니다.
AccountManagement는 가능한 한 빠른 동시 바인딩을 사용하고 자동으로 이 옵션을 설정합니다. 그림 1의 Protocols 계층 위에 AccountManagement 계층이 있는 것도 이러한 이유에서입니다. 빠른 동시 바인딩은 네트워크에서 일반 텍스트 자격 증명을 전달하는 단순 바인딩 모드에서만 사용할 수 있습니다. 따라서 보안상의 이유로 빠른 동시 바인딩은 항상 SSL과 함께 사용해야 합니다.

확장성 모델
AccountManagement가 빛을 발하는 다른 분야로 확장성 모델을 들 수 있습니다. Active Directory 도메인 서비스와 AD LDS 모두에 사용할 수 있는 사용자 지정 프로비전 시스템을 구축하기 위해 다양한 Principal 파생 클래스를 사용하는 개발자가 증가할 것입니다. 조직에서 특히 AD LDS의 경우 사용자와 그룹에 대한 자체 메타데이터를 지원하기 위해 디렉터리에 사용자 지정 스키마 확장을 추가하는 경우도 많아질 것입니다.
AccountManagement는 .NET Framework 개체 지향 설계 및 특성 기반 확장 가능 메타데이터를 사용함으로써 사용자 지정 스키마를 지원하는 사용자 지정 보안 주체 클래스를 쉽게 만들 수 있게 해 줍니다. Principal 파생 클래스 중 하나에서 상속하고 클래스와 속성을 적절한 특성으로 표시하면 사용자 지정 principal 클래스가 이러한 디렉터리 특성은 물론 기본 제공 형식에서 이미 지원되는 특성을 읽고 쓸 수 있습니다.
AccountManagement가 제공하는 확장성 메커니즘은 Active Directory 도메인 서비스 또는 AD LDS에 저장된 보안 주체에 사용하도록 설계되었다는 사실을 알아야 합니다. 즉, 비 Microsoft LDAP 디렉터리는 크게 염두에 두지 않은 것입니다. 따라서 비 Microsoft LDAP 디렉터리에서 프로비전에 사용할 프레임워크를 구축하려면 Protocols 네임스페이스의 저수준 클래스를 사용해야 합니다. 또한 SAM 스키마는 확장할 수 없으므로 로컬 SAM 계정에는 확장성 모델이 적합하지 않습니다.
표준 LDAP 사용자 클래스를 사용하여 응용 프로그램의 보안 주체를 저장하는 AD LDS 디렉터리가 있다고 가정해 보겠습니다. 그리고 msdn-subscriberID라는 사용자 개체를 식별하기 위한 특별한 특성을 지원하도록 LDAP 디렉터리 스키마를 확장했다고 가정합니다. 그림 13에서는 사용자 개체를 프로비전하고 이 특성에 대해 만들기, 읽기 및 쓰기 작업을 제공하는 사용자 지정 클래스를 작성하는 방법을 보여 줍니다.
[DirectoryObjectClass("user")]
[DirectoryRdnPrefix("CN")]
class MsdnUser : UserPrincipal
{
    public MsdnUser(PrincipalContext context)
        : base(context) { }

    public MsdnUser(
        PrincipalContext context,
        string samAccountName,
        string password,
        bool enabled
        )
        : base(
           context,
           samAccountName,
           password,
           enabled
           )
    {
    }

    [DirectoryProperty("msdn-subscriberID")]
    public string MsdnSubscriberId
    {
        get
        {
            object[] result = this.ExtensionGet("msdn-subscriberID");
            if (result != null) {
                return (string)result[0];
            }
            else {
                return null;
            }
        }
        set { this.ExtensionSet("msdn-subscriberID", value); }
    }
}

이 코드는 UserPrincipal 클래스에서 상속하며 DirectoryObjectClass 및 DirectoryRdnPrefix라는 두 가지 특성이 지정됩니다. 보안 주체 확장 클래스에는 이 두 특성이 모두 필요합니다. DirectoryObjectClass 특성은 지원되는 저장소(Active Directory 도메인 서비스 또는 AD LDS)에서 디렉터리에 이 개체의 인스턴스를 만들 때 objectClass 디렉터리 특성에 사용하는 값을 결정합니다. 여기서는 기본 AD LDS 사용자 클래스이지만 실제로는 다른 어떤 것도 될 수 있습니다. DirectoryRdnPrefix 특성은 디렉터리에서 이 클래스의 개체를 명명하는 데 사용할 RDN(상대 고유 이름) 특성 이름을 결정합니다. Active Directory 도메인 서비스에서는 RDN 접두사를 변경할 수 없으며 보안 주체 클래스의 경우 항상 CN입니다. 그러나 보다 유연한 AD LDS에서는 원하는 경우 다른 RDN을 사용할 수 있습니다.
예의 클래스에는 문자열을 반환하는 MsdnSubscriberID라는 속성이 있습니다. 이 클래스는 속성 값을 저장하는 데 사용할 LDAP 스키마 특성을 지정하는 DirectoryProperty 특성으로 표시되어 있습니다. 기본 프레임워크는 이 보안 주체 유형에 대한 검색 작업을 최적화하는 데 이 값을 사용합니다.
예의 속성 get 및 set 구현에서는 Principal 기본 클래스의 protected ExtensionGet 및 ExtensionSet 메서드를 사용하여 기본 속성 캐시에 대해 값을 읽고 씁니다. 이러한 메서드는 아직 데이터베이스/ID 저장소에 저장되지 않은 개체의 값을 메모리 내에 저장하도록 지원하고 기존 개체에서 값을 읽고 쓰는 작업도 지원합니다. LDAP 디렉터리는 다양한 특성 형식을 지원할 뿐만 아니라 특성에 여러 값이 포함될 수 있으므로 이러한 메서드는 object[] 형식을 사용하여 값을 읽고 씁니다. 이러한 유연성은 유용하지만 개체 형식의 배열에 더해 강력한 형식의 스칼라 문자열 값을 제공하려면 예 구현에서 보듯이 추가적인 작업이 필요합니다. 결과적으로 사용자 지정 MsdnUser 클래스의 사용자에게는 프로그래밍이 용이한 인터페이스가 제공됩니다.
디렉터리 스키마에 더해 강력한 형식의 값까지 제공하는 기능은 이 확장성 모델의 가장 유용한 기능 중 하나입니다. .NET Framework에서 제공하는 다양한 형식 시스템을 사용하면 단순 문자열 형식 외에도 Active Directory 도메인 서비스 jpgPhoto 특성을 일반적으로 System.DirectoryServices에서 값을 읽을 때 얻는 기본 byte[]가 아니라 System.Drawing.Image 또는 System.IO.Stream으로 나타내는 등의 작업을 수행할 수 있습니다.
이 기사의 코드 다운로드에는 이러한 기능을 보여 주는 몇 가지 다른 예가 있습니다. 또한 MsdnUser 클래스로 테스트 디렉터리를 확장하는 데 사용할 수 있는 스키마 확장도 msdnschema.ldf라는 표준 LDIF 형식 파일로 제공됩니다. "디렉터리 서비스 리소스" 보충 기사의 유용한 링크도 참조하시기 바랍니다.

결론
AccountManagement는 Microsoft에서 제공하는 풍부한 디렉터리 서비스 프로그래밍 모델에 절실히 요구되던 관리 코드 추가 기능이라 할 수 있습니다. AccountManagement 네임스페이스가 추가됨에 따라 개발자들은 일반적인 CRUD 및 검색 작업에 강력한 형식의 여러 가지 보안 주체를 사용할 수 있게 되었습니다.
이 네임스페이스에는 안전하고 성능이 뛰어난 관리 코드를 쉽게 작성할 수 있도록 디렉터리 서비스 프로그래밍과 관련된 최선의 방법이 캡슐화되어 있습니다. 또한 AccountManagement는 확장이 가능하므로 Active Directory 도메인 서비스 및 AD LDS의 사용자 지정 디렉터리 개체와 완벽하게 상호 작용할 수 있습니다.

Joe Kaplan은 Accenture의 사내 IT 조직에서 .NET Framework를 사용한 엔터프라이즈 응용 프로그램 구축을 담당하고 있습니다. 또한 응용 프로그램 보안, 페더레이션 ID 관리 및 디렉터리 서비스 프로그래밍 전문가로서 이 분야에서 Microsoft MVP로 선정되기도 하였으며 .NET Developer's Guide to Directory Services Programming(Addison-Wesley, 2006)의 공동 저자이기도 합니다.

Ethan Wilansky Microsoft Directory Services MVP이자 EDS 엔터프라이즈 설계자입니다. Ethan은 EDS Innovation Engineering의 일환으로, 현재 사용자 지정 SharePoint 응용 프로그램 개발에 집중하고 있는 개발 팀을 이끌며 EDS에 디렉터리 서비스 프로그래밍 솔루션과 관련한 자문을 제공하고 있습니다.
Posted by tornado
|