利用Spring进行LDAP验证登录遇到的问题及其解决方式

版权声明:本文为博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/t894690230/article/details/52928369

有些系统需要使用公司内部的域帐号登录,那么就需要连接LDAP进行验证,Spring Secutiry提供了使用LDAP验证的方式(就相比登录验证来说,Spring提供的LDAP验证,比自己实现LDAP验证真是麻烦了不少),能完美契合Spring Secutiry的可参考这篇文章:https://www.ibm.com/developerworks/cn/java/j-lo-springsecurity/

我主要遇到的问题是:大概SpringSecurityLdapTemplate认为使用匿名登录LDAP就能获取或验证用户信息(也可能是我没找到),所以在使用LdapUserSearch.searchForUser(username)之前,并没用提供输入用户名与密码的地方,因为它是通过获取ContextSource.getReadOnlyContext()方法来获取ContextSource的,而不是使用ContextSource.getContext(String principal, String credentials)方法,这就导致了使用匿名方式会获取不到用户信息。这个问题可以通过扩展LdapContextSource的实现类来自定义环境变量实现实名登录,如下(所有*大多为隐去的部分,可能需要替换):

package com.***.config.auth;

import java.util.Hashtable;

import javax.naming.Context;
import javax.naming.NamingException;
import javax.naming.directory.DirContext;

import org.springframework.security.ldap.ppolicy.PasswordPolicyAwareContextSource;

/**
 * 登录环境变量的设置
 */
public class LoginContextSource extends PasswordPolicyAwareContextSource {
    private static final String LDAP_FACTORY = "com.sun.jndi.ldap.LdapCtxFactory";

    public LoginContextSource(String providerUrl) {
        super(providerUrl);

        this.afterPropertiesSet();
    }

    @Override
    protected DirContext getDirContextInstance(Hashtable<String, Object> environment) throws NamingException {
        environment.put(Context.INITIAL_CONTEXT_FACTORY, LDAP_FACTORY);
        // LDAP server
        //environment.put(Context.PROVIDER_URL, ladpUrl);
        environment.put(Context.SECURITY_AUTHENTICATION, "simple");
        // 这里只是做一个演示,后面其实并不需要公用的帐号登录
        environment.put(Context.SECURITY_PRINCIPAL, "username");
        environment.put(Context.SECURITY_CREDENTIALS, "password");
        environment.put("java.naming.referral", "follow");

        return super.getDirContextInstance(environment);
    }
}

同时由于Spring Secutiry自带的验证方式并不适合自己的需求,最主要的原因是Spring Secutiry使用匿名登录进行搜索,可能导致无法搜索到用户,而又不可能提供公用账户(涉及到改密码就会比较麻烦),所以只能使用用户自己的域帐户登录后,再使用这个DirContext获取他自己的资料,那么就需要自己实现AbstractLdapAuthenticator类,并实现自己的验证逻辑,如下:

package com.***.config.auth;

import javax.naming.Context;
import javax.naming.NamingException;
import javax.naming.directory.SearchControls;
import javax.naming.ldap.LdapContext;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.ldap.core.ContextSource;
import org.springframework.ldap.core.DirContextOperations;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.ldap.SpringSecurityLdapTemplate;
import org.springframework.security.ldap.authentication.AbstractLdapAuthenticator;

/**
 * 自定义的LDAP登录认证器
 */
public class LoginAuthenticator extends AbstractLdapAuthenticator {
    private static final Log logger = LogFactory.getLog(LoginAuthenticator.class);

    /**
     * The filter expression used in the user search. This is an LDAP search filter (as
     * defined in 'RFC 2254') with optional arguments. See the documentation for the
     * <tt>search</tt> methods in {@link javax.naming.directory.DirContext DirContext} for
     * more information.
     *
     * <p>
     * In this case, the username is the only parameter.
     * </p>
     * Possible examples are:
     * <ul>
     * <li>(uid={0}) - this would search for a username match on the uid attribute.</li>
     * </ul>
     */
    private final String searchFilter;

    /** Context name to search in, relative to the base of the configured ContextSource. */
    private String searchBase = "";

    /** Default search controls */
    private SearchControls searchControls = new SearchControls();

    public LoginAuthenticator(ContextSource contextSource, String searchBase, String searchFilter) {
        super(contextSource);

        this.searchFilter = searchFilter;
        this.searchBase = searchBase;

        if (searchBase.length() == 0) {
            logger.info("SearchBase not set. Searches will be performed from the root: ---");
        }
    }

    @Override
    public DirContextOperations authenticate(Authentication authentication) {
        DirContextOperations user = null;

        String username = authentication.getName();
        String password = (String) authentication.getCredentials();

        ContextSource contextSource = getContextSource();
        LdapContext context = (LdapContext) contextSource.getReadOnlyContext();

        try {
            // 尝试使用用户的域账号登陆LDAP,如果连接成功那么就算是通过
            context.addToEnvironment(Context.SECURITY_PRINCIPAL, username + "@buyabs.corp");
            context.addToEnvironment(Context.SECURITY_CREDENTIALS, password);
            context.reconnect(null);
        } catch (NamingException e) {
            if (logger.isDebugEnabled()) {
                logger.debug("Username or password does not match: " + e.getLocalizedMessage());
            }
            // 如果重新连接不上,则断定为登陆失败
            throw new UsernameNotFoundException("Username or password does not match: " + e.getLocalizedMessage());
        }

        // 使用用户自己的域账号登陆LDAP,并获取信息。避免使用公用账号获取信息(因为我们压根没有公用账号0_0)
        if (user == null && getUserSearch() != null) {
            try {
                searchControls.setSearchScope(SearchControls.SUBTREE_SCOPE);
                user = SpringSecurityLdapTemplate.searchForSingleEntryInternal(context, searchControls, searchBase, searchFilter, new String[] { username });
            } catch (NamingException e) {
                if (logger.isDebugEnabled()) {
                    logger.debug("Username or password does not match: " + e.getLocalizedMessage());
                }
            }
        }

        if (user == null) {
            throw new UsernameNotFoundException("User not found: " + username);
        }

        return user;
    }

}

同时,还需要扩展LdapAuthoritiesPopulator(权限处理)类,如下:

package com.***.config.auth;

import java.util.Collection;
import java.util.List;

import javax.annotation.Resource;

import org.springframework.context.annotation.Scope;
import org.springframework.ldap.core.DirContextOperations;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.ldap.userdetails.LdapAuthoritiesPopulator;
import org.springframework.stereotype.Service;

import com.newegg.dba.smartdatastack.db.dao.UserInfoDAO;
import com.newegg.dba.smartdatastack.db.vo.UserRoleVO;

/**
 * Spring Secutity 登陆时,获取权限的实现
 */
@Service("ldapAuthoritiesPopulator")
@Scope("prototype")
public class PortalLdapAuthoritiesPopulator implements LdapAuthoritiesPopulator {

    @Resource(name="userInfoDAO")
    private UserInfoDAO userInfoDAO;

    @Override
    public Collection<? extends GrantedAuthority> getGrantedAuthorities(DirContextOperations userData,
            String username) {

        List<UserRoleVO> roleLs = userInfoDAO.findRoleByUserName(username);

        return roleLs;
    }
}

这样,基本的扩展类就已经准备好了,再建一个Builder类,如下:

package com.***.auth;

import java.util.Properties;

import javax.annotation.Resource;

import org.springframework.context.annotation.Scope;
import org.springframework.ldap.core.support.BaseLdapPathContextSource;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.ldap.authentication.LdapAuthenticationProvider;
import org.springframework.security.ldap.search.FilterBasedLdapUserSearch;
import org.springframework.stereotype.Service;

@Service("authenticatorProviderBuilder")
@Scope("prototype")
public class AuthenticatorProviderBuilder {
    @Resource(name="ldapAuthoritiesPopulator")
    PortalLdapAuthoritiesPopulator ldapAuthoritiesPopulator; 

    @Resource(name="profileSetting")
    Properties setting;

    public AuthenticationProvider getAuthenticationProvider() {
        String ladpDomain = setting.getProperty("ladp.domain");
        String ladpuserSearch = setting.getProperty("ladp.userSearch");
        String ladpUrl = setting.getProperty("ladp.url");

        BaseLdapPathContextSource contenxSource = new LoginContextSource(ladpUrl);

        FilterBasedLdapUserSearch userSearch = new FilterBasedLdapUserSearch(ladpDomain, ladpuserSearch, contenxSource);

        LoginAuthenticator bindAuth = new LoginAuthenticator(contenxSource, ladpDomain, ladpuserSearch);
        bindAuth.setUserSearch(userSearch);

        LdapAuthenticationProvider ldapAuth = new LdapAuthenticationProvider(bindAuth, ldapAuthoritiesPopulator);

        return ldapAuth;
    }
}

最后,就是配置了,因为使用的代码配置,所以其代码如下:

package com.***.config;

import javax.annotation.Resource;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;

import com.***.config.auth.AuthenticatorProviderBuilder;

@Configuration
@EnableWebSecurity
public class SpringSecurityConfiguration extends WebSecurityConfigurerAdapter {

    @Resource(name="authenticatorProviderBuilder")
    private AuthenticatorProviderBuilder authenticatorProviderBuilder; 

    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth)
            throws Exception {
        //  配置LDAP的验证方式
        auth.authenticationProvider(authenticatorProviderBuilder.getAuthenticationProvider());
    }

    @Override
    public void configure(WebSecurity web) throws Exception {
        ...
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        ...
    }

}

总结就是:由于自己所处的系统的原因,所以相比起来,Spring自带的LDAP验证登录还不如自己使用javax.naming.*下的类来实现灵活,附上使用这种方式测试时的源码吧:

package com.***.ldap;

import java.util.Hashtable;

import javax.naming.Context;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.directory.Attribute;
import javax.naming.directory.SearchControls;
import javax.naming.directory.SearchResult;
import javax.naming.ldap.InitialLdapContext;
import javax.naming.ldap.LdapContext;

/**
 * User AD 
 * <p>
 *  用于连接LDAP管理用户信息等操作
 * </p>
 * @author kt94
 *
 */
public class UserAD {
    private static final String LDAP_FACTORY = "com.sun.jndi.ldap.LdapCtxFactory";
    private static final String LDAP_URL = "ldap://127.0.0.1:389/";
    private static final String LDAP_DOMAIN = "dc=***,dc=***";
    private static final String PUBLIC_ACCOUNT = "username";
    private static final String PUBLIC_PASSWORD = "password";

    public static void main(String[] args) throws NamingException {
        UserAD userAD = new UserAD();

        NamingEnumeration<SearchResult> en = userAD.searchBySortName("kt94", 
                "name", "***", "***", "***", "***", "***", "***", "***");
        // NamingEnumeration<SearchResult> en = userAD.searchBySortName("kt94");

        if (en == null) {
            System.out.println("Have no NamingEnumeration.");
        }

        if (!en.hasMoreElements()) {
            System.out.println("Have no element.");
        } else {
            // 输出查到的数据
            while (en.hasMoreElements()) {
                SearchResult result = en.next();
                NamingEnumeration<? extends Attribute> attrs = result.getAttributes().getAll();
                while (attrs.hasMore()) {
                    Attribute attr = attrs.next();

                    if ("manager".equals(attr.getID())) {
                        String[] manArr = attr.get().toString().split(",");
                        if (manArr.length > 0) {
                            String[] manAttrArr = manArr[0].split("=");
                            if (manAttrArr.length > 1) {
                                System.out.println(attr.getID() + "=" + manAttrArr[1]);
                            }
                        }
                    } else {
                        System.out.println(attr.getID() + "=" + attr.get());
                    }
                }

                System.out.println("======================");

            }
        }

        boolean authenticate = userAD.authenticate("kttt", "111");
        System.out.println("authenticate: " + authenticate);
    }

    /**
     * 登陆认证
     * @param sortName
     * @param password
     * @return
     */
    public boolean authenticate(String sortName, String password) {
        if (sortName == null || "".equals(sortName)) {
            return false;
        }

        String account = sortName + "@***.***";

        LdapContext ladpContent = connectLdap("", "");
        try {
            ladpContent.addToEnvironment(Context.SECURITY_PRINCIPAL, account);
            ladpContent.addToEnvironment(Context.SECURITY_CREDENTIALS, password);
            ladpContent.reconnect(null);
        } catch (NamingException e) {
            return  false;
        }

        return true;
    }

    /**
     * 根据短名搜索用户
     * @param sortName
     * @param attributes
     * @return
     */
    public NamingEnumeration<SearchResult> searchBySortName(String sortName, String... attributes) {
        String filter = "(&(objectclass=user)(objectcategory=user)(sAMAccountName=" + sortName + "))";

        return search(filter, attributes);
    }

    /**
     * 根据英文名称搜索用户
     * @param name
     * @param attributes
     * @return
     */
    public NamingEnumeration<SearchResult> searchByName(String name, String... attributes) {
        String filter = "(&(objectclass=user)(objectcategory=user)(name=" + name + "))";

        return search(filter, attributes);
    }

    /**
     * 根据搜索条件搜索
     * @param filter - Filter, @see <a href="http://go.microsoft.com/fwlink/?LinkID=143553">LDAP syntax help</a>
     * @param attributes - Returning attributes
     * @return
     */
    public NamingEnumeration<SearchResult> search(String filter, String... attributes) {
        SearchControls searchControls = new SearchControls();
        searchControls.setSearchScope(SearchControls.SUBTREE_SCOPE);

        if (attributes != null && attributes.length > 0) {
            searchControls.setReturningAttributes(attributes);
        }

        LdapContext ladpContent = connectLdap(PUBLIC_ACCOUNT, PUBLIC_PASSWORD);

        NamingEnumeration<SearchResult> en = null;
        try {
            // 三个参数分别为:1.上下文;2.要搜索的属性,如果为空或 null,则返回目标上下文中的所有对象;3.控制搜索的搜索控件,如果为 null,则使用默认的搜索控件
            en = ladpContent.search(LDAP_DOMAIN, filter, searchControls);
        } catch (NamingException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }

        return en;
    }

    /**
     * 连接LDAP
     * @param account - [Short name]@buyabs.corp
     * @param password - Your windows password
     * @return
     */
    public LdapContext connectLdap(String account, String password) {
        Hashtable<String, String> env = getLdapEnvironmentConfig(account, password);

        LdapContext context = null;

        try {
            context = new InitialLdapContext(env, null);
        } catch (NamingException e) {
            // TODO Auto-generated catch block
            // 连接失败日志打印
            e.printStackTrace();
        }

        return context;
    }

    /**
     * 获取LDAP环境配置
     * @param account
     * @param password
     * @return
     */
    private Hashtable<String, String> getLdapEnvironmentConfig(String account, String password) {
        Hashtable<String, String> env = new Hashtable<String, String>();
        env.put(Context.INITIAL_CONTEXT_FACTORY, LDAP_FACTORY);
        // LDAP server
        env.put(Context.PROVIDER_URL, LDAP_URL);
        env.put(Context.SECURITY_AUTHENTICATION, "simple");
        env.put(Context.SECURITY_PRINCIPAL, account);
        env.put(Context.SECURITY_CREDENTIALS, password);
        env.put("java.naming.referral", "follow");

        return env;
    }
}

着重说明:用户名不只是登录Windows的帐号,而是要加上@…

展开阅读全文

没有更多推荐了,返回首页