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

Active Directory Webservices for DevOps

starIconstarIconstarIconstarIconstarIcon

5.00/5 (5投票s)

2015 年 10 月 27 日

Apache

4分钟阅读

viewsIcon

23072

downloadIcon

497

活动目录 Web 服务,处理身份验证、单点登录 (SSO) 以及原生例程之外的其他应用程序。

引言

我发现创建一个 Web 服务来处理与活动目录相关的大部分日常工作很有意思,例如添加/删除用户/组/计算机,启用/禁用用户/计算机等。 这样做的想法是,无需每次需要这些微型例程时都实现它们,只需使用 Web 服务即可。但我实现这个东西的真正原因是身份验证,以及能够为希望使用此 Web 服务的所有应用程序提供 SSO。 这种实现并非最终版本,但它提供了如何扩展以包含所有与活动目录相关的例程的想法,这使得系统管理员在想要自动化任务(例如创建 AD 用户以及邮箱和启用 Lync)时,生活更轻松。 之前提到的那些任务可以使用 PowerShell 完成,如果不是全部,那么大多数任务都可以完成,但为了能够向应用程序提供此类功能,通过 Web 服务来完成它已经足够方便了。

背景

我在这里所做的是为活动目录例程实现 WCF Web 服务。 大多数例程都是 ComputerPrincipalUserPrincipalGroupPrincipalDirectoryEntry 的包装器,这些都可在以下命名空间下使用

using System.DirectoryServices;
using System.DirectoryServices.AccountManagement;
using System.DirectoryServices.ActiveDirectory; 

除此之外,还为包含 用户名密码会话开始 日期的 XML 序列化对象的会话实现 AES 加密。

Using the Code

代码很长,所以我会尽量保持主要内容。

代码被分成两类

  1. 与身份验证相关 (包括会话处理和加密)
  2. 活动目录例程

因此,我们将从身份验证相关开始,首先是会话处理,其中包含一个 可序列化结构,其中包含 SessionData,然后处理程序处理 SessionData 结构的序列化和反序列化,我不会在这里包含加密和解密,因为它可以被您希望实现的任何对称加密算法替换。 产生的加密会话将与每个活动目录例程的每个请求一起发送。

    [Serializable]
    public struct SessionData
    {
        public string Username { get; set; }
        public string Password { get; set; }
        public DateTime SessionStart { get; set; }
    }

    public class SessionHandler
    {       
        private static string SerializeObject<t>(SessionData sessionData)
        {
            string serialized = string.Empty;
            XmlSerializer serializer = new XmlSerializer( typeof( T ) );

            using ( StringWriter writer = new StringWriter() )
            {
                serializer.Serialize( writer , sessionData );

                byte[] data = Encoding.UTF8.GetBytes( writer.ToString() );

                return Convert.ToBase64String( data );
            }
        }

        private static T DesrializeObject<t>( string data )
        {
            return (T)DesrializeObject( data , typeof( T ) );
        }

        private static object DesrializeObject( string objectData , Type type )
        {
            var serializer = new XmlSerializer( type );
            object result;

            using ( TextReader reader = new StringReader( objectData ) )
            {
                result = serializer.Deserialize( reader );
            }

            return result;
        }

        private static string Encrypt(string session)
        {
           string sessionCypher = Cryptor.Encrypt( session );

           return sessionCypher;
        }      

        private static string Decrypt( string session ) 
        {
            string decrptedSession = Cryptor.Decrypt( session );
            return decrptedSession;
        }

        public static string EncryptSession(SessionData session)
        {
            string serializedSession = SerializeObject<sessiondata>( session );
            string encryptedSession = Encrypt( serializedSession );

            return encryptedSession;            
        }

        public static SessionData DecryptSession( string session ) 
        {
            string decryptedSession = Decrypt( session );
            SessionData desrialized = DesrializeObject<sessiondata>( decryptedSession );
            return desrialized; 
        }
    }

当用户使用其活动目录凭据进行身份验证时,一切都开始,这会返回一个加密会话,作为包含 IsAuthenticatedMessageSessionKey 的 JSON 对象,AuthData 类包含 用户名密码

public Session AuthenticateUserUsingCredentials( AuthDataRequest authData )
{
    UserInfoResponse userInfo = new UserInfoResponse();
    string emailAddress = authData.username;
    string password = authData.password;

    Session stat = new Session();

    string msg = string.Empty;

    if ( string.IsNullOrEmpty( emailAddress ) || string.IsNullOrEmpty( password ) )
    {
        stat.Message = "Email and/or password can't be empty!";
        stat.IsAuthenticated = false;

        return stat;
    }
    try
    {
        userInfo = GetUserAttributes( emailAddress );

        if ( userInfo == null )
        {
            stat.Message = "Error: Couldn't fetch user information!";
            stat.IsAuthenticated = false;

            return stat;
        }

        var directoryEntry = new DirectoryEntry( LocalGcUri , userInfo.Upn , password );

        directoryEntry.AuthenticationType = AuthenticationTypes.None;

        var localFilter = string.Format( AdSearchFilter , emailAddress );

        var localSearcher = new DirectorySearcher( directoryEntry );

        localSearcher.PropertiesToLoad.Add( "mail" );
        localSearcher.Filter = localFilter;

        var result = localSearcher.FindOne();

        if ( result != null )
        {
            stat.Message = "You have logged in successfully!";
            stat.IsAuthenticated = true;

            //Set the session Data
            SessionData session = new SessionData();

            session.Username = userInfo.EmailAddress;
            session.Password = password;
            session.SessionStart = DateTime.Now;

            //Encrypt Session Data
            stat.SessionKey = SessionHandler.EncryptSession( session );

            return stat;
        }

        stat.Message = "Login failed, please try again.";
        stat.IsAuthenticated = false;

        return stat;
    }
    catch ( Exception ex )
    {
        stat.Message = "Wrong Email and/or Password " + ex;
        stat.IsAuthenticated = false;

        return stat;
    }
}
并且定义了 Web 服务的身份验证接口,以将输入和输出序列化为 JSON
[OperationContract]
[WebInvoke( 
    UriTemplate = "auth/user" ,  
    RequestFormat= WebMessageFormat.Json,  
    ResponseFormat = WebMessageFormat.Json,
    BodyStyle = WebMessageBodyStyle.Bare,
    Method = "POST" )]
Session AuthenticateUserUsingCredentials
  ( [MessageParameter( Name = "authdata" )] AuthDataRequest authData );

请注意,在您成功进行身份验证后,您将能够使用会话密钥对使用相同 Web 服务进行身份验证的不同应用程序进行身份验证,这可以按如下方式完成

Web 服务 OperationContract 定义

[OperationContract]
[WebInvoke(
    UriTemplate = "auth/session" , 
    ResponseFormat = WebMessageFormat.Json ,
    RequestFormat = WebMessageFormat.Json ,
    BodyStyle = WebMessageBodyStyle.Bare,
    Method="POST")]
Session AuthenticateUserUsingSession
        ( [MessageParameter( Name = "sessionkey" )] string sessionKey );

至于实现,描述如下

public Session AuthenticateUserUsingSession( string sessionKey )
{
    return ValidateSession( sessionKey );
}

此函数只是 SessionValidation 的包装器,会话将基于两个因素进行验证

  1. 成功解密 SessionKey
  2. 会话的有效性不超过可配置的 TTL,就我而言,是两个小时。
public Session ValidateSession( string sessionKey )
{
    Session stat = new Session();

    if ( string.IsNullOrWhiteSpace( sessionKey ) )
    {
        stat.Message = "No Session key has been provide";
        stat.IsAuthenticated = false;

        return stat;
    }
    else
    {
        try
        {
            SessionData sessionData = SessionHandler.DecryptSession( sessionKey );

            if ( sessionKey != null && ( ( DateTime.Now.Subtract
               ( sessionData.SessionStart ) ).TotalHours < SessionTTL ) )
            {
                stat.Message = "You have logged in successfully!";
                stat.IsAuthenticated = true;
                stat.SessionKey = sessionKey;
                return stat;
            }
            else
            {
                AuthDataRequest authData = new AuthDataRequest();
                authData.username = sessionData.Username;
                authData.password = sessionData.Password;

                stat = AuthenticateUserUsingCredentials( authData );
                stat.Message = "You have logged in successfully!, 
                                and Session key has been renewed";

                return stat;
            }
        }
        catch ( Exception ex )
        {
            stat.Message = "Couldn't validate Session key, 
                            kindly authenticate first " + ex;
            stat.IsAuthenticated = false;

            return stat;
        }
    }
}

就身份验证而言,我认为我们可以在这里停止,然后转到活动目录例程,请注意,我不会列出所有例程,因为它们很多,所以我将坚持几个例子。

AddUser

首先,我们定义一个具有 DataMembersDataContract

[DataContract]
public class RequestUserCreate
{
    [DataMember]
    public string FirstName { get; set; }

    [DataMember]
    public string LastName { get; set; }

    [DataMember]
    public string UserLogonName { get; set; }

    [DataMember]
    public string EmployeeID { get; set; }

    [DataMember]
    public string EmailAddress { get; set; }

    [DataMember]
    public string Telephone { get; set; }

    [DataMember]
    public string Address { get; set; }

    [DataMember]
    public string PostalCode { get; set; }

    [DataMember]
    public string PostOfficeBox { get; set; }

    [DataMember]
    public string PhysicalDeliveryOffice { get; set; }

    [DataMember]
    public string Country { get; set; }

    [DataMember]
    public string City { get; set; }

    [DataMember]
    public string Title { get; set; }

    [DataMember]
    public string Department { get; set; }

    [DataMember]
    public string Company { get; set; }

    [DataMember]
    public string Description { get; set; }

    [DataMember]
    public string PhoneExtention { get; set; }

    [DataMember]
    public string PhoneIpAccessCode { get; set; }

    [DataMember]
    public string Password { get; set; }

    [DataMember]
    public DomainRequest DomainInfo { get; set; }
}

我认为您会注意到有一个名为 DomainRequestObject,它包括连接到域控制器或连接到全局目录所需的所有信息,除了在成功进行身份验证后保存会话密钥之外。

[DataContract]
public class DomainRequest
{
    [DataMember]
    public string ADHost { get; set; }

    [DataMember]
    public string DomainName { get; set; }

    [DataMember]
    public string ContainerPath { get; set; }

    [DataMember]
    public string BindingUserName { get; set; }

    [DataMember]
    public string BindingUserPassword { get; set; }

    [DataMember]
    public string SessionKey { get; set; }
}

但是,既然我们已经有了会话密钥并且已经通过身份验证了,为什么还要添加用户名和密码呢,答案很简单,因为您可能对该特定域容器没有写入或读取权限,因此您可以使用不同的用户名运行这些命令,但您必须已经过身份验证并且会话密钥有效。 另外要提到的是,它始终检查您是否有权访问该特定容器。

然后,我们定义 OperationContract

[OperationContract]
[WebInvoke(
    UriTemplate = "ad/account/add" ,
    RequestFormat = WebMessageFormat.Json ,
    ResponseFormat = WebMessageFormat.Json ,
    BodyStyle = WebMessageBodyStyle.Bare ,
    Method = "POST" )]
ResponseMessage AddADUser( [MessageParameter( Name = "userinfo" )] 
                            RequestUserCreate userinfo );

实现将如下所示

public ResponseMessage AddADUser( RequestUserCreate userinfo )
{
    ResponseMessage status = new ResponseMessage();

    status.IsSuccessful = false;
    status.Message = string.Empty;

    Session stat = ValidateSession( userinfo.DomainInfo.SessionKey );

    if ( stat.IsAuthenticated == true )
    {
        PrincipalContext principalContext = null;

        string uri = FixADURI( userinfo.DomainInfo.ADHost , 
                               userinfo.DomainInfo.ContainerPath );

        if ( string.IsNullOrWhiteSpace( uri ) )
        {
            status.Message = status.Message = 
              "AD Host is not allowed to be empty, kindly provide the AD Host";
            return status;
        }

        bool isAllowWite = CheckWriteOermission( uri , 
        userinfo.DomainInfo.BindingUserName , userinfo.DomainInfo.BindingUserPassword );

        try
        {
            UserPrincipal usr = 
                FindADUser( userinfo.UserLogonName , userinfo.DomainInfo );
            if ( usr != null )
            {
                status.Message = " user already exists. 
                                   Please use a different User Logon Name";
                return status;
            }
            else
            {
                principalContext = new PrincipalContext
                ( ContextType.Domain , userinfo.DomainInfo.DomainName , 
                  userinfo.DomainInfo.ContainerPath , 
                  userinfo.DomainInfo.BindingUserName , 
                  userinfo.DomainInfo.BindingUserPassword );
            }
        }
        catch ( Exception ex )
        {
            status.Message = @"Failed to create PrincipalContext: " + ex;
            return status;
        }

        // Create the new UserPrincipal object
        UserPrincipal userPrincipal = new UserPrincipal( principalContext );

        if ( !string.IsNullOrWhiteSpace( userinfo.LastName ) )
            userPrincipal.Surname = userinfo.LastName;

        if ( !string.IsNullOrWhiteSpace( userinfo.FirstName ) )
            userPrincipal.GivenName = userinfo.FirstName;

        if ( !string.IsNullOrWhiteSpace( userinfo.LastName ) && 
             !string.IsNullOrWhiteSpace( userinfo.FirstName ) )
            userPrincipal.DisplayName = userinfo.FirstName + " " + userinfo.LastName;

        if ( !string.IsNullOrWhiteSpace( userinfo.Description ) )
            userPrincipal.Description = userinfo.Description;

        if ( !string.IsNullOrWhiteSpace( userinfo.EmployeeID ) )
            userPrincipal.EmployeeId = userinfo.EmployeeID;

        if ( !string.IsNullOrWhiteSpace( userinfo.EmailAddress ) )
            userPrincipal.EmailAddress = userinfo.EmailAddress;

        if ( !string.IsNullOrWhiteSpace( userinfo.Telephone ) )
            userPrincipal.VoiceTelephoneNumber = userinfo.Telephone;

        if ( !string.IsNullOrWhiteSpace( userinfo.UserLogonName ) )
            userPrincipal.SamAccountName = userinfo.UserLogonName;

        if ( !string.IsNullOrWhiteSpace( userinfo.Password ) )
            userPrincipal.SetPassword( userinfo.Password );

        userPrincipal.Enabled = true;
        userPrincipal.ExpirePasswordNow();

        try
        {
            userPrincipal.Save();

            DirectoryEntry de = (DirectoryEntry)userPrincipal.GetUnderlyingObject();

            FillUserExtraAttributes( ref de , userinfo );

            de.CommitChanges();
            status.Message = "Account has been created successfully";
            status.IsSuccessful = true;
        }
        catch ( Exception ex )
        {
            status.Message = "Exception creating user object. " + ex;
            status.IsSuccessful = false;
            return status;
        }

        return status;
    }
    else
    {
        status.Message = "Kindly authenticate first";
        return status;
    }
}

在这里,我使用 UserPrincipal 使用基本信息填充用户对象,保存它并返回类型为 DirectoryEntry UnderlyingObject 以填充其余信息,额外的属性函数如下所示

private void FillUserExtraAttributes
        ( ref DirectoryEntry de , RequestUserCreate userinfo ) 
{
    try 
    {
        if ( !string.IsNullOrWhiteSpace( userinfo.Title ) )
            de.Properties[ "title" ].Value = userinfo.Title;

        if ( !string.IsNullOrWhiteSpace( userinfo.City ) )
            de.Properties[ "l" ].Value = userinfo.City;

        if ( !string.IsNullOrWhiteSpace( userinfo.Country ) )
            de.Properties[ "c" ].Value = userinfo.Country;

        if ( !string.IsNullOrWhiteSpace( userinfo.PostalCode ) )
            de.Properties[ "postalCode" ].Value = userinfo.PostalCode;

        if ( !string.IsNullOrWhiteSpace( userinfo.PostOfficeBox ) )
            de.Properties[ "postOfficeBox" ].Value = userinfo.PostOfficeBox;

        if ( !string.IsNullOrWhiteSpace( userinfo.Address ) )
            de.Properties[ "streetAddress" ].Value = userinfo.Address;

        if ( !string.IsNullOrWhiteSpace( userinfo.Department ) )
            de.Properties[ "department" ].Value = userinfo.Department;

        if ( !string.IsNullOrWhiteSpace( userinfo.PhysicalDeliveryOffice ) )
            de.Properties[ "physicalDeliveryOfficeName" ].Value = 
                                            userinfo.PhysicalDeliveryOffice;

        if ( !string.IsNullOrWhiteSpace( userinfo.Company ) )
            de.Properties[ "company" ].Value = userinfo.Company;

        if ( !string.IsNullOrWhiteSpace( userinfo.PhoneExtention ) )
            de.Properties[ "extensionAttribute1" ].Value = userinfo.PhoneExtention;

        if ( !string.IsNullOrWhiteSpace( userinfo.PhoneIpAccessCode ) )
            de.Properties[ "extensionAttribute2" ].Value = userinfo.PhoneIpAccessCode;
    }
    catch ( Exception ex ) 
    {
        throw ex;
    }
}

Web 服务定义

WCF Web 服务已通过 REST 和 SOAP 公开,以便为开发人员提供选择其使用方式的灵活性,至于 *web.conf*,配置如下所示

<configuration>
  <appSettings>
    <add key="aspnet:UseTaskFriendlySynchronizationContext" value="true" />
    <add key="LocalDomainURI" value="GC://x.x.x.x" />
    <add key="LocalDomainUser" value="bind-user" />
    <add key="LocalDomainPassword" value="bind-password" />
    <add key="ADSearchFilter" 
     value="(&(objectClass=user)(objectCategory=person)(mail={0}))" />
    <add key="MailHost" value="X.X.X.X />
    <add key="ReplyTo" value="xxx@cexample.com" />
    <add key="NotificationsEmail" value="xxx@example.com" />
    <add key="AesKey" value="AES KEY"/>
    <add key="AesIV" value="AES IV"/>
    <add key="SessionTTL" value="2"/>
  </appSettings>
  <system.web>
    <compilation debug="true" targetFramework="4.5" />
    <httpRuntime targetFramework="4.5"/>
  </system.web>
  <system.serviceModel>
    <services>
      <service name="ADWS.Adws">
        <endpoint address="rest" behaviorConfiguration="webBehaviour"
          binding="webHttpBinding" name="RESTEndPoint" contract="ADWS.IAdws" />
        <endpoint address="soap" binding="basicHttpBinding" name="SOAPEndPoint"
          contract="ADWS.IAdws" />
        <endpoint address="mex" binding="mexHttpBinding" contract="IMetadataExchange" />
      </service>
    </services>
    <behaviors>
      <serviceBehaviors>
        <behavior>
          <serviceMetadata httpGetEnabled="true" httpsGetEnabled="true"/>
          <serviceDebug includeExceptionDetailInFaults="true"/>
        </behavior>
      </serviceBehaviors>
      <endpointBehaviors>
        <behavior name="webBehaviour">
          <webHttp  helpEnabled="true"/>
        </behavior>
      </endpointBehaviors>
    </behaviors>
    <protocolMapping>
      <add binding="webHttpBinding" scheme="http" />
    </protocolMapping>
    <serviceHostingEnvironment aspNetCompatibilityEnabled="true" 
                               multipleSiteBindingsEnabled="true" />
  </system.serviceModel>
  <system.webServer>
    <security>
      <requestFiltering allowDoubleEscaping="true"/>
    </security>
    <modules runAllManagedModulesForAllRequests="true"/>
    <httpProtocol>
      <customHeaders>
        <add name="Access-Control-Allow-Origin" value="*" />
        <add name="Access-Control-Allow-Headers" value="Content-Type, Accept" />
      </customHeaders>
    </httpProtocol>
    <directoryBrowse enabled="true"/>
  </system.webServer>
</configuration>

AES 密钥和 IV 应在此处提供,为了安全起见,如果有人访问了 *web.conf*,您可以加密 *web.conf*,请参考这篇文章 以获取有关该主题的更多信息。

历史

  • 版本 1.0
DevOps 的活动目录 Web 服务 - CodeProject - 代码之家
© . All rights reserved.