كيف يمكنك المصادقة ضد خادم Active Directory باستخدام Spring Security؟

StackOverflow https://stackoverflow.com/questions/84680

سؤال

أنا أكتب تطبيق ويب Spring الذي يتطلب من المستخدمين تسجيل الدخول.تمتلك شركتي خادم Active Directory الذي أرغب في استخدامه لهذا الغرض.ومع ذلك، أواجه مشكلة في استخدام Spring Security للاتصال بالخادم.

أنا أستخدم Spring 2.5.5 وSpring Security 2.0.3، بالإضافة إلى Java 1.6.

إذا قمت بتغيير عنوان URL لـ LDAP إلى عنوان IP خاطئ، فلن يؤدي ذلك إلى حدوث استثناء أو أي شيء، لذا أتساءل عما إذا كان صحيحًا محاولة للاتصال بالخادم للبدء.

على الرغم من أن تطبيق الويب يبدأ التشغيل بشكل جيد، إلا أنه يتم رفض أي معلومات أدخلها في صفحة تسجيل الدخول.لقد استخدمت InMemoryDaoImpl سابقًا، وكان يعمل بشكل جيد، لذا يبدو أن بقية التطبيق قد تم تكوينه بشكل صحيح.

فيما يلي الفاصوليا المتعلقة بالأمان:

  <beans:bean id="ldapAuthProvider" class="org.springframework.security.providers.ldap.LdapAuthenticationProvider">
    <beans:constructor-arg>
      <beans:bean class="org.springframework.security.providers.ldap.authenticator.BindAuthenticator">
        <beans:constructor-arg ref="initialDirContextFactory" />
        <beans:property name="userDnPatterns">
          <beans:list>
            <beans:value>CN={0},OU=SBSUsers,OU=Users,OU=MyBusiness,DC=Acme,DC=com</beans:value>
          </beans:list>
        </beans:property>
      </beans:bean>
    </beans:constructor-arg>
  </beans:bean>

  <beans:bean id="userDetailsService" class="org.springframework.security.userdetails.ldap.LdapUserDetailsManager">
    <beans:constructor-arg ref="initialDirContextFactory" />
  </beans:bean>

  <beans:bean id="initialDirContextFactory" class="org.springframework.security.ldap.DefaultInitialDirContextFactory">
    <beans:constructor-arg value="ldap://192.168.123.456:389/DC=Acme,DC=com" />
  </beans:bean>
هل كانت مفيدة؟

المحلول

لقد مررت بنفس تجربة الضرب التي قمت بها، وانتهى بي الأمر بكتابة موفر مصادقة مخصص يقوم باستعلام LDAP على خادم Active Directory.

لذا فإن الفاصوليا المتعلقة بالأمان هي:

<beans:bean id="contextSource"
    class="org.springframework.security.ldap.DefaultSpringSecurityContextSource">
    <beans:constructor-arg value="ldap://hostname.queso.com:389/" />
</beans:bean>

<beans:bean id="ldapAuthenticationProvider"
    class="org.queso.ad.service.authentication.LdapAuthenticationProvider">
    <beans:property name="authenticator" ref="ldapAuthenticator" />
    <custom-authentication-provider />
</beans:bean>

<beans:bean id="ldapAuthenticator"
    class="org.queso.ad.service.authentication.LdapAuthenticatorImpl">
    <beans:property name="contextFactory" ref="contextSource" />
    <beans:property name="principalPrefix" value="QUESO\" />
</beans:bean>

ثم فئة LdapAuthenticationProvider:

/**
 * Custom Spring Security authentication provider which tries to bind to an LDAP server with
 * the passed-in credentials; of note, when used with the custom {@link LdapAuthenticatorImpl},
 * does <strong>not</strong> require an LDAP username and password for initial binding.
 * 
 * @author Jason
 */
public class LdapAuthenticationProvider implements AuthenticationProvider {

    private LdapAuthenticator authenticator;

    public Authentication authenticate(Authentication auth) throws AuthenticationException {

        // Authenticate, using the passed-in credentials.
        DirContextOperations authAdapter = authenticator.authenticate(auth);

        // Creating an LdapAuthenticationToken (rather than using the existing Authentication
        // object) allows us to add the already-created LDAP context for our app to use later.
        LdapAuthenticationToken ldapAuth = new LdapAuthenticationToken(auth, "ROLE_USER");
        InitialLdapContext ldapContext = (InitialLdapContext) authAdapter
                .getObjectAttribute("ldapContext");
        if (ldapContext != null) {
            ldapAuth.setContext(ldapContext);
        }

        return ldapAuth;
    }

    public boolean supports(Class clazz) {
        return (UsernamePasswordAuthenticationToken.class.isAssignableFrom(clazz));
    }

    public LdapAuthenticator getAuthenticator() {
        return authenticator;
    }

    public void setAuthenticator(LdapAuthenticator authenticator) {
        this.authenticator = authenticator;
    }

}

ثم فئة LdapAuthenticatorImpl:

/**
 * Custom Spring Security LDAP authenticator which tries to bind to an LDAP server using the
 * passed-in credentials; does <strong>not</strong> require "master" credentials for an
 * initial bind prior to searching for the passed-in username.
 * 
 * @author Jason
 */
public class LdapAuthenticatorImpl implements LdapAuthenticator {

    private DefaultSpringSecurityContextSource contextFactory;
    private String principalPrefix = "";

    public DirContextOperations authenticate(Authentication authentication) {

        // Grab the username and password out of the authentication object.
        String principal = principalPrefix + authentication.getName();
        String password = "";
        if (authentication.getCredentials() != null) {
            password = authentication.getCredentials().toString();
        }

        // If we have a valid username and password, try to authenticate.
        if (!("".equals(principal.trim())) && !("".equals(password.trim()))) {
            InitialLdapContext ldapContext = (InitialLdapContext) contextFactory
                    .getReadWriteContext(principal, password);

            // We need to pass the context back out, so that the auth provider can add it to the
            // Authentication object.
            DirContextOperations authAdapter = new DirContextAdapter();
            authAdapter.addAttributeValue("ldapContext", ldapContext);

            return authAdapter;
        } else {
            throw new BadCredentialsException("Blank username and/or password!");
        }
    }

    /**
     * Since the InitialLdapContext that's stored as a property of an LdapAuthenticationToken is
     * transient (because it isn't Serializable), we need some way to recreate the
     * InitialLdapContext if it's null (e.g., if the LdapAuthenticationToken has been serialized
     * and deserialized). This is that mechanism.
     * 
     * @param authenticator
     *          the LdapAuthenticator instance from your application's context
     * @param auth
     *          the LdapAuthenticationToken in which to recreate the InitialLdapContext
     * @return
     */
    static public InitialLdapContext recreateLdapContext(LdapAuthenticator authenticator,
            LdapAuthenticationToken auth) {
        DirContextOperations authAdapter = authenticator.authenticate(auth);
        InitialLdapContext context = (InitialLdapContext) authAdapter
                .getObjectAttribute("ldapContext");
        auth.setContext(context);
        return context;
    }

    public DefaultSpringSecurityContextSource getContextFactory() {
        return contextFactory;
    }

    /**
     * Set the context factory to use for generating a new LDAP context.
     * 
     * @param contextFactory
     */
    public void setContextFactory(DefaultSpringSecurityContextSource contextFactory) {
        this.contextFactory = contextFactory;
    }

    public String getPrincipalPrefix() {
        return principalPrefix;
    }

    /**
     * Set the string to be prepended to all principal names prior to attempting authentication
     * against the LDAP server.  (For example, if the Active Directory wants the domain-name-plus
     * backslash prepended, use this.)
     * 
     * @param principalPrefix
     */
    public void setPrincipalPrefix(String principalPrefix) {
        if (principalPrefix != null) {
            this.principalPrefix = principalPrefix;
        } else {
            this.principalPrefix = "";
        }
    }

}

وأخيرًا، فئة LdapAuthenticationToken:

/**
 * <p>
 * Authentication token to use when an app needs further access to the LDAP context used to
 * authenticate the user.
 * </p>
 * 
 * <p>
 * When this is the Authentication object stored in the Spring Security context, an application
 * can retrieve the current LDAP context thusly:
 * </p>
 * 
 * <pre>
 * LdapAuthenticationToken ldapAuth = (LdapAuthenticationToken) SecurityContextHolder
 *      .getContext().getAuthentication();
 * InitialLdapContext ldapContext = ldapAuth.getContext();
 * </pre>
 * 
 * @author Jason
 * 
 */
public class LdapAuthenticationToken extends AbstractAuthenticationToken {

    private static final long serialVersionUID = -5040340622950665401L;

    private Authentication auth;
    transient private InitialLdapContext context;
    private List<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>();

    /**
     * Construct a new LdapAuthenticationToken, using an existing Authentication object and
     * granting all users a default authority.
     * 
     * @param auth
     * @param defaultAuthority
     */
    public LdapAuthenticationToken(Authentication auth, GrantedAuthority defaultAuthority) {
        this.auth = auth;
        if (auth.getAuthorities() != null) {
            this.authorities.addAll(Arrays.asList(auth.getAuthorities()));
        }
        if (defaultAuthority != null) {
            this.authorities.add(defaultAuthority);
        }
        super.setAuthenticated(true);
    }

    /**
     * Construct a new LdapAuthenticationToken, using an existing Authentication object and
     * granting all users a default authority.
     * 
     * @param auth
     * @param defaultAuthority
     */
    public LdapAuthenticationToken(Authentication auth, String defaultAuthority) {
        this(auth, new GrantedAuthorityImpl(defaultAuthority));
    }

    public GrantedAuthority[] getAuthorities() {
        GrantedAuthority[] authoritiesArray = this.authorities.toArray(new GrantedAuthority[0]);
        return authoritiesArray;
    }

    public void addAuthority(GrantedAuthority authority) {
        this.authorities.add(authority);
    }

    public Object getCredentials() {
        return auth.getCredentials();
    }

    public Object getPrincipal() {
        return auth.getPrincipal();
    }

    /**
     * Retrieve the LDAP context attached to this user's authentication object.
     * 
     * @return the LDAP context
     */
    public InitialLdapContext getContext() {
        return context;
    }

    /**
     * Attach an LDAP context to this user's authentication object.
     * 
     * @param context
     *          the LDAP context
     */
    public void setContext(InitialLdapContext context) {
        this.context = context;
    }

}

ستلاحظ أن هناك بعض الأجزاء التي قد لا تحتاج إليها.

على سبيل المثال، يحتاج تطبيقي إلى الاحتفاظ بسياق LDAP الذي تم تسجيل الدخول إليه بنجاح لاستخدامه مرة أخرى من قبل المستخدم بمجرد تسجيل الدخول - والغرض من التطبيق هو السماح للمستخدمين بتسجيل الدخول عبر بيانات اعتماد AD الخاصة بهم ثم تنفيذ المزيد من الوظائف المتعلقة بـ AD.ولهذا السبب، لدي رمز مصادقة مخصص، LdapAuthenticationToken، أقوم بتمريره (بدلاً من رمز المصادقة الافتراضي في Spring) والذي يسمح لي بإرفاق سياق LDAP.في LdapAuthenticationProvider.authenticate()، أقوم بإنشاء هذا الرمز المميز وتمريره مرة أخرى؛في LdapAuthenticatorImpl.authenticate()، أقوم بإرفاق سياق تسجيل الدخول بكائن الإرجاع بحيث يمكن إضافته إلى كائن مصادقة Spring الخاص بالمستخدم.

أيضًا، في LdapAuthenticationProvider.authenticate()، أقوم بتعيين دور ROLE_USER لجميع المستخدمين الذين قاموا بتسجيل الدخول - وهذا ما يسمح لي بعد ذلك باختبار هذا الدور في عناصر عنوان URL للاعتراض.ستحتاج إلى جعل هذا يطابق الدور الذي تريد اختباره، أو حتى تعيين الأدوار بناءً على مجموعات Active Directory أو أي شيء آخر.

أخيرًا، وكنتيجة طبيعية لذلك، فإن الطريقة التي طبقت بها LdapAuthenticationProvider.authenticate() تمنح جميع المستخدمين الذين لديهم حسابات AD صالحة نفس دور ROLE_USER.من الواضح، بهذه الطريقة، يمكنك إجراء المزيد من الاختبارات على المستخدم (على سبيل المثال، هل المستخدم في مجموعة إعلانية معينة؟) وتعيين الأدوار بهذه الطريقة، أو حتى اختبار بعض الشروط قبل منح المستخدم حق الوصول إلى الجميع.

نصائح أخرى

كمرجع، يحتوي Spring Security 3.1 على موفر مصادقة خصيصا للدليل النشط.

فقط لجلب هذا إلى حالة محدثة.يحتوي Spring Security 3.0 على حزمة كاملة مع التطبيقات الافتراضية المخصصة لـ ldap-bind بالإضافة إلى الاستعلام ومقارنة المصادقة.

تمكنت من المصادقة على الدليل النشط باستخدام Spring Security 2.0.4.

لقد قمت بتوثيق الإعدادات

http://maniezhilan.blogspot.com/2008/10/spring-security-204-with-active.html

كما في إجابة لوقا أعلاه:

يحتوي Spring Security 3.1 على موفر مصادقة مخصص لـ Active Directory.

فيما يلي تفاصيل كيفية القيام بذلك بسهولة باستخدام ActiveDirectoryLdapAuthenticationProvider.

في الموارد.رائع:

ldapAuthProvider1(ActiveDirectoryLdapAuthenticationProvider,
        "mydomain.com",
        "ldap://mydomain.com/"
)

في التكوين.groovy:

grails.plugin.springsecurity.providerNames = ['ldapAuthProvider1']

هذا هو كل الكود الذي تحتاجه.يمكنك إلى حد كبير إزالة جميع إعدادات Grails.plugin.springsecurity.ldap.* الأخرى في Config.groovy لأنها لا تنطبق على إعداد AD هذا.

للتوثيق راجع:http://docs.spring.io/spring-security/site/docs/3.1.x/reference/springsecurity-single.html#ldap-active-directory

مصادقة LDAP بدون SSL ليست آمنة حيث يمكن لأي شخص رؤية بيانات اعتماد المستخدم عند نقلها إلى خادم LDAP.أقترح استخدام بروتوكول LDAPS:\ للمصادقة.لا يتطلب الأمر أي تغيير كبير في الجزء الربيعي ولكن قد تواجه بعض المشكلات المتعلقة بالشهادات.يرى مصادقة LDAP Active Directory في الربيع باستخدام SSL لمزيد من التفاصيل

من إجابة لوقا أعلاه:

للرجوع إليها ، لدى Spring Security 3.1 مزود مصادقة [خصيصًا لـ Active Directory] [1].

[1]: http://static.springsource.org/spring-security/site/docs/3.1.x/reference/springsecurity-single.html#ldap-active-directory

لقد جربت ما سبق مع Spring Security 3.1.1:هناك بعض التغييرات الطفيفة من ldap - تأتي مجموعات الدليل النشط التي يكون المستخدم عضوًا فيها كحالة أصلية.

في السابق، كانت المجموعات في إطار ldap تُكتب بالأحرف الكبيرة وتبدأ بـ "ROLE_"، مما جعل من السهل العثور عليها من خلال البحث عن نص في مشروع ما، ولكن من الواضح أنها قد تؤدي إلى حدوث مشكلات في مجموعة يونكس إذا كانت هناك، لسبب غريب، مجموعتان منفصلتان يتم التمييز بينهما فقط حسب الحالة ( أي الحسابات والحسابات).

كما يتطلب بناء الجملة مواصفات يدوية لاسم وحدة تحكم المجال والمنفذ، مما يجعل التكرار مخيفًا بعض الشيء.بالتأكيد هناك طريقة للبحث عن سجل SRV DNS للمجال في Java، أي ما يعادل (من Samba 4 howto):

$ host -t SRV _ldap._tcp.samdom.example.com.
_ldap._tcp.samdom.example.com has SRV record 0 100 389 samba.samdom.example.com.

متبوعًا بالبحث العادي:

$ host -t A samba.samdom.example.com.
samba.samdom.example.com has address 10.0.0.1

(في الواقع قد تحتاج إلى البحث عن سجل _kerberos SRV أيضًا...)

ما ورد أعلاه كان مع Samba4.0rc1، ونحن نقوم بالترقية تدريجيًا من بيئة Samba 3.x LDAP إلى Samba AD one.

إذا كنت تستخدم الربيع الأمن 4 يمكنك أيضًا تنفيذ نفس الشيء باستخدام فئة معينة

  • SecurityConfig.java
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {


static final Logger LOGGER = LoggerFactory.getLogger(SecurityConfig.class);

@Autowired
protected void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
    auth.authenticationProvider(activeDirectoryLdapAuthenticationProvider());
}

@Override
protected void configure(HttpSecurity http) throws Exception {
    http
            .authorizeRequests()
              .antMatchers("/").permitAll()
              .anyRequest().authenticated();
            .and()
              .formLogin()
            .and()
              .logout();
}

@Bean
public AuthenticationProvider activeDirectoryLdapAuthenticationProvider() {
    ActiveDirectoryLdapAuthenticationProvider authenticationProvider = 
        new ActiveDirectoryLdapAuthenticationProvider("<domain>", "<url>");

    authenticationProvider.setConvertSubErrorCodesToExceptions(true);
    authenticationProvider.setUseAuthenticationRequestCredentials(true);

    return authenticationProvider;
}
}
مرخصة بموجب: CC-BY-SA مع الإسناد
لا تنتمي إلى StackOverflow
scroll top