Spring Security 3.1 Active Directory身份验证

时间:2012-02-13 09:22:06

标签: authentication active-directory spring-security

我正在使用下一个配置连接到我的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)
....

3 个答案:

答案 0 :(得分: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且已有补丁。

答案 1 :(得分:2)

错误IncorrectResultSizeDataAccessException是由org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl

中的错误引起的

如果查看以下代码,当令牌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);
        }
    }

}

以下是弹簧安全的可行配置:

<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 :(得分:1)

我在尝试对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>