Spring Security 3.1 活动目录认证

Posted

技术标签:

【中文标题】Spring Security 3.1 活动目录认证【英文标题】:Spring Security 3.1 Active Directory Authentication 【发布时间】:2012-03-04 17:00:48 【问题描述】:

我正在使用下一个配置连接到我的 AD:

    class="org.springframework.security.ldap.authentication.ad.ActiveDirectoryLdapAuthenticationProvider">
    <beans:constructor-arg value="mydomain" />
    <beans:constructor-arg value="ldap://my URL :389" />
    <beans:property name="convertSubErrorCodesToExceptions" value="true"/>
</beans:bean>

连接工作正常,因为如果我输入了错误的登录名/密码,我会得到“错误凭据”(在目录中找不到用户)

但如果我尝试使用正确的登录名和密码,我会得到一个异常:

org.springframework.dao.IncorrectResultSizeDataAccessException: Incorrect result size: expected 1, actual 0
    at org.springframework.security.ldap.SpringSecurityLdapTemplate.searchForSingleEntryInternal(SpringSecurityLdapTemplate.java:239)
    at org.springframework.security.ldap.authentication.ad.ActiveDirectoryLdapAuthenticationProvider.searchForUser(ActiveDirectoryLdapAuthenticationProvider.java:258)
....

【问题讨论】:

您收到的错误表明在 searchForSingleEntryInternal 中查找用户名/密码失败。如果您在第 210 行设置断点,请查看调用 ctx.search(searchBaseDn, filter, params, searchControls);的参数值是什么; 还可以尝试使用外部 LDAP 工具(例如 ldaptool.sourceforge.net),看看您是否可以在测试设置中使用域上的用户名和密码进行 LDAP 绑定。 【参考方案1】:

错误 IncorrectResultSizeDataAccessException 是由 org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl 中的错误引起的

如果查看以下代码,当 token seriesId 不存在时,不应该抛出错误“多个值”。

public PersistentRememberMeToken getTokenForSeries(String seriesId) 
    try 
        return (PersistentRememberMeToken) tokensBySeriesMapping.findObject(seriesId);
     catch(IncorrectResultSizeDataAccessException moreThanOne) 
        logger.error("Querying token for series '" + seriesId + "' returned more than one value. Series" +
                " should be unique");
     catch(DataAccessException e) 
        logger.error("Failed to load token for series " + seriesId, e);
    

    return null;

您可以实现自己的令牌存储库 dao,这是我的:

/**
 * Save/cache the login token, retrieve or update it for remember-me feature.
 * 
 * create table persistent_logins (username varchar(64) not null, series varchar(64) primary key,
 * token varchar(64) not null, last_used timestamp not null)
 * 
 * @author lchen
 * 
 */
public class TokenRepositoryDao extends BaseDao implements PersistentTokenRepository 

    @Override
    public void createNewToken(PersistentRememberMeToken token) 
        String sql = "insert into persistent_logins (username, series, token, last_used) values(?,?,?,?)";
        getJdbcTemplate().update(sql, token.getUsername(), token.getSeries(), token.getTokenValue(), token.getDate());
    

    @Override
    public PersistentRememberMeToken getTokenForSeries(String series) 
        String sql = "select username,series,token,last_used from persistent_logins where series = ?";
        try 
            return getJdbcTemplate().queryForObject(sql, new PersistentRememberMeTokenMapper(), series);
         catch (IncorrectResultSizeDataAccessException moreThanOne) 
            if (moreThanOne.getActualSize() > 1)
                logger.error("Querying token for series '" + series + "' returned more than one value. Series" + " should be unique");
         catch (DataAccessException e) 
            logger.error("Failed to load token for series " + series, e);
        
        return null;
    

    @Override
    public void removeUserTokens(String username) 
        String sql = "delete from persistent_logins where username = ?";
        getJdbcTemplate().update(sql, username);
    

    @Override
    public void updateToken(String series, String tokenValue, Date lastUsed) 
        String sql = "update persistent_logins set token = ?, last_used = ? where series = ?";
        getJdbcTemplate().update(sql, tokenValue, new Date(), series);
    

    private class PersistentRememberMeTokenMapper implements RowMapper<PersistentRememberMeToken> 
        @Override
        public PersistentRememberMeToken mapRow(ResultSet rs, int rowNum) throws SQLException 
            String username = rs.getString("username");
            String series = rs.getString("series");
            String token = rs.getString("token");
            Date date = rs.getDate("last_used");
            return new PersistentRememberMeToken(username, series, token, date);
        
    


以下是 Spring Security 的可行配置:

<security:http pattern="/common/**" security="none" />
<security:http pattern="/styles/**" security="none" />
<security:http pattern="/images/**" security="none" />
<security:http pattern="/scripts/**" security="none" />
<security:http pattern="/layouts/**" security="none" />

<security:http use-expressions="true">
    <security:intercept-url pattern="/login.do" access="permitAll" />
    <security:intercept-url pattern="/logout.do" access="permitAll" />
    <security:intercept-url pattern="/login/failure.do" access="permitAll" />
    <security:intercept-url pattern="/index.jsp" access="permitAll" />
    <security:intercept-url pattern="/home/**" access="isAuthenticated()" />
    <security:intercept-url pattern="/upload/**" access="hasRole('ROLE_USER')" />
    <security:intercept-url pattern="/**" access="denyAll" />
    <security:form-login login-page="/login.do" authentication-failure-url="/login/failure.do" default-target-url="/" />
    <security:logout logout-url="/logout.do" logout-success-url="/" delete-cookies="JSESSIONID" />
    <security:remember-me user-service-ref="userDetailsService" token-repository-ref="tokenRepository" token-validity-seconds="1296000" />
</security:http>

<bean id="tokenRepository" class="com.abc.dao.TokenRepositoryDao" />

<security:authentication-manager>
    <security:authentication-provider ref="ldapAuthProvider" />
</security:authentication-manager>

<bean id="userDetailsService" class="org.springframework.security.ldap.userdetails.LdapUserDetailsService">
    <constructor-arg ref="userSearch" />
    <constructor-arg ref="authoritiesPopulator" />
</bean>

<bean id="contextSource" class="org.springframework.security.ldap.DefaultSpringSecurityContextSource">
    <constructor-arg value="ldap://corp.abc.com:389/dc=Corp,dc=abc,dc=com" />
    <property name="userDn" value="***" />
    <property name="password" value="***" />
    <property name="baseEnvironmentProperties">
        <map>
            <entry key="java.naming.referral">
                <value>follow</value> <!-- Avoid error: Unprocessed Continuation Reference(s); remaining name '' -->
            </entry>
        </map>
    </property>
</bean>

<bean id="userSearch" class="org.springframework.security.ldap.search.FilterBasedLdapUserSearch">
    <constructor-arg>
        <value></value> <!-- blank value is required here! -->
    </constructor-arg>
    <constructor-arg>
        <value>(sAMAccountName=0)</value>
    </constructor-arg>
    <constructor-arg ref="contextSource" />
    <property name="searchSubtree">
        <value>true</value>
    </property>
</bean>

<bean id="ldapAuthProvider" class="org.springframework.security.ldap.authentication.LdapAuthenticationProvider">
    <constructor-arg ref="authenticator" />
    <constructor-arg ref="authoritiesPopulator" />
</bean>

<bean id="authenticator" class="org.springframework.security.ldap.authentication.BindAuthenticator">
    <constructor-arg ref="contextSource" />
    <property name="userDnPatterns">
        <list>
            <value>sAMAccountName=0</value>
        </list>
    </property>
    <property name="userSearch" ref="userSearch" />
</bean>

<bean id="authoritiesPopulator" class="org.springframework.security.ldap.userdetails.DefaultLdapAuthoritiesPopulator">
    <constructor-arg ref="contextSource" />
    <constructor-arg value="" /> <!-- From the root DN of the context factory -->
    <property name="groupRoleAttribute" value="cn" />
    <property name="rolePrefix" value="ROLE_" />
    <property name="searchSubtree" value="true" />
    <property name="convertToUpperCase" value="true" />
    <property name="ignorePartialResultException">
        <value>false</value>
    </property>
</bean>

【讨论】:

【参考方案2】:

我在尝试针对 Active Directory 进行身份验证时遇到了同样的问题IncorrectResultSizeDataAccessException。我没有直接解决这个特定问题,但我已经实现了一个解决方法,它功能齐全,但确实意味着您需要一个“服务帐户”用户名和密码才能与 AD 建立通信。我猜它使用的是“通用”Spring LDAP 方法,而不是特殊的 AD 方法。

我按照这里的食谱:

Active Directory Spring Security XML config, on the SpringSource forum

这是我的security-context.xml 文件,供参考:

<?xml version="1.0" encoding="UTF-8"?>
<beans:beans xmlns="http://www.springframework.org/schema/security"
xmlns:beans="http://www.springframework.org/schema/beans"     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.1.xsd
http://www.springframework.org/schema/security
http://www.springframework.org/schema/security/spring-security-3.1.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-3.1.xsd
">

<!-- There is some Java based config here, don't forget. -->
<!-- Its not important for this example-->
<context:component-scan base-package="uk.ac.example.ldaptest.security" />

<!-- This is for our Active Dir LDAP implementation -->
<beans:bean id="contextSource"
    class="org.springframework.ldap.core.support.LdapContextSource">
    <beans:property name="url"
        value="LDAP://ads.ntd.example.ac.uk:389" />
    <beans:property name="base" value="dc=ntd,dc=example,dc=ac,dc=uk" />
    <beans:property name="userDn" value="cn=ldap,ou=Service Accounts,ou=Management,ou=example,dc=ntd,dc=example,dc=ac,dc=uk" />
    <beans:property name="password" value="XXXXXXXXX" />
    <beans:property name="pooled" value="true" />
    <!-- AD Specific Setting for avoiding the partial exception error -->
    <beans:property name="referral" value="follow" />
</beans:bean>

<beans:bean id="ldapAuthenticationProvider"
    class="org.springframework.security.ldap.authentication.LdapAuthenticationProvider">
    <beans:constructor-arg>
        <beans:bean
            class="org.springframework.security.ldap.authentication.BindAuthenticator">
            <beans:constructor-arg ref="contextSource" />
            <beans:property name="userSearch">
                <beans:bean id="userSearch"
                    class="org.springframework.security.ldap.search.FilterBasedLdapUserSearch">
                    <beans:constructor-arg index="0" value="" />
                    <beans:constructor-arg index="1" value="(sAMAccountName=0)" />
                    <beans:constructor-arg index="2" ref="contextSource" />
                </beans:bean>
            </beans:property>
        </beans:bean>
    </beans:constructor-arg>
    <beans:constructor-arg>
        <beans:bean
            class="org.springframework.security.ldap.userdetails.DefaultLdapAuthoritiesPopulator">
            <beans:constructor-arg ref="contextSource" />
            <beans:constructor-arg value="" />
            <beans:property name="groupSearchFilter" value="(member=0)" />
            <beans:property name="searchSubtree" value="true" />
            <!-- Below Settings convert the adds the prefix ROLE_ to roles returned 
                from AD -->
        </beans:bean>
    </beans:constructor-arg>
    <!-- Create the Mapper object that returns our customised User object -->
    <!-- Set up in the Java based config mentioned earlier -->
    <beans:property name="userDetailsContextMapper" ref="myUdcm" />
</beans:bean>

<beans:bean id="authenticationManager"
    class="org.springframework.security.authentication.ProviderManager">
    <beans:constructor-arg>
        <beans:list>
            <beans:ref local="ldapAuthenticationProvider" />
        </beans:list>
    </beans:constructor-arg>
</beans:bean>

<!-- we want all URLs within our application to be secured, requiring the 
    role ROLE_STAFF to access them. LDAP supplies this -->
<http auto-config="true" use-expressions="true"
    authentication-manager-ref="authenticationManager">
    <intercept-url pattern="/resources/**" access="permitAll" />
    <intercept-url pattern="/**" access="hasRole('ROLE_STAFF')" />
    <session-management>
        <concurrency-control max-sessions="1" />
    </session-management>
</http>

【讨论】:

【参考方案3】:

检查使用的搜索过滤器是否与您的活动目录记录一致。

我最近在我的网络应用程序中遇到了同样的异常。用户凭据正确且 ActiveDirectoryLdapAuthenticationProvider 绑定/身份验证正确。在为已验证的记录搜索组和其他属性时绑定后发生故障。

如果您查看 ActiveDirectoryLdapAuthenticationProvider 中的代码,它具有用于搜索过滤器的硬编码值,并且它始终使用绑定主体进行搜索。

这个方法

private DirContextOperations searchForUser(DirContext ctx, String username) throws NamingException 
    SearchControls searchCtls = new SearchControls();
    searchCtls.setSearchScope(SearchControls.SUBTREE_SCOPE);

    String searchFilter = "(&(objectClass=user)(userPrincipalName=0))";

    final String bindPrincipal = createBindPrincipal(username);

    String searchRoot = rootDn != null ? rootDn : searchRootFromPrincipal(bindPrincipal);

    return SpringSecurityLdapTemplate.searchForSingleEntryInternal(ctx, searchCtls, searchRoot, searchFilter,
            new Object[]bindPrincipal);

一个Jira issue 已经提交并且已经有补丁了。

【讨论】:

非常感谢您的帮助。抱歉,我对这种技术很陌生。我应该怎么做才能使用那个补丁?谢谢!! 如果使用 Eclipse,您可以按照以下步骤操作 1. 从上面的 Jira 链接下载补丁 2. 下载 Spring Security 的源代码并将其准备为 Eclipse 项目 3. 在 Eclipse 中右键单击项目并转到团队-> 应用补丁。这会将补丁应用于 Spring 源代码,然后您可以构建和使用它。我最终所做的是将我感兴趣的源代码复制到我自己的包中的自己的项目中,并在我自己的源代码中添加一个 new Spring bean 来替换原来的 Spring bean。我刚刚看到 Jira 问题仍未解决。 @AlbertoFernandez 非常感谢。我和你做的一样。我在自己的代码中创建了一个 Spring bean,替换了我需要的。 这很有帮助。我不确定更好的方法,但我刚刚创建了 CustomADLdapAuthenticationProvider,从 spring 安全库复制 ActiveDirectoryLdapAuthenticationProvider。

以上是关于Spring Security 3.1 活动目录认证的主要内容,如果未能解决你的问题,请参考以下文章

带有 apacheds 的示例活动目录 ldif 文件

Spring Security 自定义配置

Spring Security 3.1 xsd 和 jars 不匹配问题

spring security 3.1 实现权限控制

spring 3.1 with hibernate 4 with spring security 3.1:如何确保包含所有依赖项以及要包含哪些标签?

Gateway 整合 Spring Security鉴权