You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Spring Security LDAP配置单元测试编写及空指针问题求助

Spring Security LDAP认证配置单元测试空指针问题解决指导

我是Spring Security新手,以下是我的LDAP认证配置代码:

@Override
protected void configure(AuthenticationManagerBuilder authBuilder) throws Exception {
    if (ldap) {
        authBuilder.ldapAuthentication()
                .userDetailsContextMapper(customUserDetailsCxtMapper())
                .userSearchFilter("(&(objectClass=user)(" + userDnPatterns + "))")
                .contextSource()
                .url(ldapProviderUrl + ldapBaseDn.trim().replaceAll(" ", "%20"))
                .managerDn(ldapSecurityPrincipal)
                .managerPassword(ldapPrincipalPassword);
    } else {
        super.configure(auth);
    }
}

@Bean
public UserDetailsContextMapper customUserDetailsContextMapper() {
    return new UserDetailsContextMapper() {
        @Override
        public UserDetails mapUserFromContext(DirContextOperations context, String uName,
                                              Collection<? extends GrantedAuthority> authorities) {
            ArrayList<GrantedAuthority> grantedAuthorities = new ArrayList<>();
            Collection<? extends GrantedAuthority> authorities = extractAuthorityFromMemberOf(context);
            grantedAuthorities.addAll(authorities);
            UserDetailsContextMapper contextMapper = new LdapUserDetailsMapper();
            return contextMapper.mapUserFromContext(context, uName, grantedAuthorities);
        }

        @Override
        public void mapUserToContext(UserDetails userDetails, DirContextAdapter dirContextAdapter) {
        }
    };
}

我想编写单元测试验证该LDAP配置,但执行userDetailsContextMapper(customUserDetailsCxtMapper())时总是出现空指针,不清楚如何mockuserDetailsContextMapper(customUserDetailsCxtMapper()).userSearchFilter("(&(objectClass=user)(" + userDnPatterns + "))").contextSource()这些代码,寻求解决指导。


1. 先修正代码中的潜在问题

你的customUserDetailsContextMapper()方法存在变量重定义问题:方法参数的authorities和内部声明的authorities重名,这会导致逻辑混乱,先修正这个问题:

@Bean
public UserDetailsContextMapper customUserDetailsContextMapper() {
    return new UserDetailsContextMapper() {
        @Override
        public UserDetails mapUserFromContext(DirContextOperations context, String uName,
                                              Collection<? extends GrantedAuthority> authorities) {
            ArrayList<GrantedAuthority> grantedAuthorities = new ArrayList<>();
            // 重命名变量避免冲突
            Collection<? extends GrantedAuthority> ldapAuthorities = extractAuthorityFromMemberOf(context);
            grantedAuthorities.addAll(ldapAuthorities);
            UserDetailsContextMapper contextMapper = new LdapUserDetailsMapper();
            return contextMapper.mapUserFromContext(context, uName, grantedAuthorities);
        }

        @Override
        public void mapUserToContext(UserDetails userDetails, DirContextAdapter dirContextAdapter) {
        }
    };
}

2. 优先选择集成测试(避免复杂Mock)

LDAP配置涉及多组件协作,纯单元测试Mock成本高,推荐用嵌入式LDAP服务器做集成测试,更贴近真实场景:

测试依赖准备

确保测试包中包含:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-test</artifactId>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>com.unboundid</groupId>
    <artifactId>unboundid-ldapsdk</artifactId>
    <scope>test</scope>
</dependency>

编写集成测试类

@SpringBootTest
@EnableConfigurationProperties
public class LdapAuthenticationConfigTest {

    @Autowired
    private AuthenticationManager authenticationManager;

    private InMemoryDirectoryServer ldapServer;

    @BeforeEach
    void setupLdapServer() throws Exception {
        // 初始化嵌入式LDAP服务器,添加测试用户和权限数据
        ldapServer = new InMemoryDirectoryServer("dc=example,dc=com");
        ldapServer.add("ou=users,dc=example,dc=com", "objectClass", "organizationalUnit");
        ldapServer.add("cn=testUser,ou=users,dc=example,dc=com", 
                       "objectClass", "user", 
                       "sAMAccountName", "testUser", 
                       "userPassword", "testPass",
                       "memberOf", "cn=adminGroup,ou=groups,dc=example,dc=com");
        ldapServer.startListening(33389);
        // 同步配置类中的LDAP地址为嵌入式服务器地址
        System.setProperty("ldap.provider.url", "ldap://localhost:33389/");
    }

    @AfterEach
    void shutdownLdapServer() {
        ldapServer.shutDown(true);
    }

    @Test
    void testLdapAuthenticationSuccess() throws AuthenticationException {
        // 构造认证请求
        Authentication authRequest = UsernamePasswordAuthenticationToken.unauthenticated("testUser", "testPass");
        // 执行认证
        Authentication authResult = authenticationManager.authenticate(authRequest);
        // 验证认证结果
        assert authResult.isAuthenticated();
        assert authResult.getName().equals("testUser");
        // 验证权限是否正确提取
        assert authResult.getAuthorities().stream().anyMatch(a -> a.getAuthority().equals("ROLE_ADMIN"));
    }
}

3. 纯单元测试方案(Mock链式调用)

如果一定要做纯单元测试,需要逐层Mock链式调用的返回对象,避免空指针:

@ExtendWith(MockitoExtension.class)
public class LdapAuthenticationConfigUnitTest {

    @InjectMocks
    private YourSecurityConfig securityConfig; // 替换成你的实际配置类名

    @Mock
    private AuthenticationManagerBuilder authBuilder;

    @Mock
    private LdapAuthenticationProviderConfigurer<AuthenticationManagerBuilder> ldapConfigurer;

    @Mock
    private ContextSourceBuilder contextSourceBuilder;

    @BeforeEach
    void setupMockChains() {
        // 配置链式调用的Mock返回值,确保每一步都不返回null
        when(authBuilder.ldapAuthentication()).thenReturn(ldapConfigurer);
        when(ldapConfigurer.userDetailsContextMapper(any(UserDetailsContextMapper.class))).thenReturn(ldapConfigurer);
        when(ldapConfigurer.userSearchFilter(anyString())).thenReturn(ldapConfigurer);
        when(ldapConfigurer.contextSource()).thenReturn(contextSourceBuilder);
        when(contextSourceBuilder.url(anyString())).thenReturn(contextSourceBuilder);
        when(contextSourceBuilder.managerDn(anyString())).thenReturn(contextSourceBuilder);
        when(contextSourceBuilder.managerPassword(anyString())).thenReturn(contextSourceBuilder);
    }

    @Test
    void testConfigureLdapAuthentication() throws Exception {
        // 开启LDAP开关
        securityConfig.setLdap(true); // 假设你的配置类有ldap属性的setter方法
        // 调用配置方法
        securityConfig.configure(authBuilder);
        // 验证所有配置方法都被正确调用
        verify(authBuilder).ldapAuthentication();
        verify(ldapConfigurer).userDetailsContextMapper(any(UserDetailsContextMapper.class));
        verify(ldapConfigurer).userSearchFilter("(&(objectClass=user)(your-dn-pattern))"); // 替换成实际的userDnPatterns值
        verify(contextSourceBuilder).url(anyString());
        verify(contextSourceBuilder).managerDn(anyString());
        verify(contextSourceBuilder).managerPassword(anyString());
    }
}

关键注意点

  • 空指针的核心原因是Mock链式调用时,某一步返回了null,导致后续方法调用失败,必须确保每一步链式调用的Mock返回值都正确设置。
  • 集成测试能更真实地验证LDAP认证的完整流程,包括用户信息、权限提取等逻辑,比纯单元测试更有价值。
  • 检查配置类中的ldap、userDnPatterns等配置属性,确保测试环境中这些属性被正确注入,避免因属性为null导致搜索过滤器拼接出错。

内容的提问来源于stack exchange,提问作者cNgamba

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.21 08:58:11