利用Spring进行LDAP验证登录遇到的问题及其解决方式
2016-10-25 22:47
543 查看
有些系统需要使用公司内部的域帐号登录,那么就需要连接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的实现类来自定义环境变量实现实名登录,如下(所有*大多为隐去的部分,可能需要替换):
同时由于Spring Secutiry自带的验证方式并不适合自己的需求,最主要的原因是Spring Secutiry使用匿名登录进行搜索,可能导致无法搜索到用户,而又不可能提供公用账户(涉及到改密码就会比较麻烦),所以只能使用用户自己的域帐户登录后,再使用这个DirContext获取他自己的资料,那么就需要自己实现AbstractLdapAuthenticator类,并实现自己的验证逻辑,如下:
同时,还需要扩展LdapAuthoritiesPopulator(权限处理)类,如下:
这样,基本的扩展类就已经准备好了,再建一个Builder类,如下:
最后,就是配置了,因为使用的代码配置,所以其代码如下:
总结就是:由于自己所处的系统的原因,所以相比起来,Spring自带的LDAP验证登录还不如自己使用javax.naming.*下的类来实现灵活,附上使用这种方式测试时的源码吧:
着重说明:用户名不只是登录Windows的帐号,而是要加上@…
我主要遇到的问题是:大概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的帐号,而是要加上@…
相关文章推荐
- 单点登录(十一)-----遇到问题-----cas启用mongodb验证方式报错--Unable to locate Spring NamespaceHandler for XML schema na
- 单点登录(十一)-----遇到问题-----cas启用mongodb验证方式报错--Unable to locate Spring NamespaceHandler for XML schema na
- Ubuntu不能利用ssh进行远程连接的解决方案及其解决过程中出现的问题
- vue遇到过的坑(父组件向输入框组件传值进去,然后子组件利用传进来的数据进行数据绑定的问题与解决方法)
- Spring-利用ThreadLocal解决线程安全问题(多线程并发登录)
- 用hibernate进行MYSQL数据存储时遇到的中文编码问题及其解决方
- Communicator使用TLS方式登录提示证书验证失败问题的解决
- 解决在ssh或telnet等方式进行远程管理遇到的乱码问题
- 单点登录(十二)-----遇到问题-----cas启用mongodb验证方式登录后没反应-pac4j-mongo包中的MongoAuthenticatInvocationTargetException
- 安装LDAP Server过程中遇到的问题及其解决方法
- 单点登录(十二)-----遇到问题-----cas启用mongodb验证方式登录后没反应-pac4j-mongo包中的MongoAuthenticatInvocationTargetException
- JAVAWEB开发之Hibernate详解(三)——Hibernate的检索方式、抓取策略以及利用二级缓存进行优化、解决数据库事务并发问题
- SSH框架登录注册页面用struts的xml配置方式校验遇到的问题及解决方法
- JAVAWEB开发之Hibernate详解(三)——Hibernate的检索方式、抓取策略以及利用二级缓存进行优化、解决数据库事务并发问题
- 利用codeception过程中遇到的问题及解决方式
- 在使用putty工具进行远程登录时也许会遇到一些问题,下面列出了一些问题有利于帮助大家解决:
- Oracle中的两种验证方式:操作系统验证和密码文件验证,通过操作系统验证的方式解决客户端登录不了数据的问题
- SQLServer2000遇到的两个问题及其解决办法
- 严重: Error listenerStart的解决办法---使用Spring时遇到的问题
- PD导入Model到Oracle9i遇到的问题及其解决方法