Spring Security Active Directory LDAP Example

说明

最近有一个用户需求,让我利用Spring Security结合他们内部的Active Directory (AD) LDAP服务器完成网页程序授权机制.

我搜了很多资料与踏了无数的坑终于找到了适合他们项目环境的配置。根据我找到的这些资料所涵盖的评论信息,我所碰到的问题貌似其他人也有碰到。现在这个例子就是我最终是如何让Spring Security与Active Directory LDAP进行整合并达到目的的。

注意:这篇文章不会涉及到Spring Security的细节与实现,具体你可以查看Spring Security文档:Spring Security 4.0.3 Documentation.

版本

此例子用到了以下版本.

  • Spring Security: 4.0.3.RELEASE
  • Spring (web, core, etc): 4.2.3.RELEASE

配置

MAVEN依赖

<!-- Spring Security -->
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-web</artifactId>
    <version>4.0.3.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-config</artifactId>
    <version>4.0.3.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-ldap</artifactId>
    <version>4.0.3.RELEASE</version>
</dependency>

部署相关(web.xml)

添加Spring Security过滤器到web.xml配置中.

<filter>
    <filter-name>springSecurityFilterChain</filter-name>
    <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
    <filter-name>springSecurityFilterChain</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

另外,确认你的配置是通过ContextLoaderListener进行加载的,并且DispatcherServlet没有进行重复加载。

<context-param>
    <param-name>contextConfigLocation</param-name>
    <!-- replace with your spring context file -->
    <param-value>/WEB-INF/mvc-dispatcher-servlet.xml</param-value>
</context-param>
<listener>
    <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>

<servlet>
    <servlet-name>mvc-dispatcher</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <init-param>
        <!-- set to blank to ensure context is only loaded once -->
        <param-name>contextConfigLocation</param-name>
        <param-value></param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
    <servlet-name>mvc-dispatcher</servlet-name>
    <!-- url base for your spring app, not security config.
        That is handled by the filter -->
    <url-pattern>/*</url-pattern>
</servlet-mapping>

注意:我碰到过一个问题,当我添加到web.xml之后,Spring context会尝试加载,然后报错并卸载。 为解决这个问题,我把Spring context文件移到并使用了ContextLoaderListener,具体看以上配置。

Spring Context

配置Spring context:

<beans xmlns="http://www.springframework.org/schema/beans"
    ...
    xmlns:sec="http://www.springframework.org/schema/security"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
        ...
        http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security.xsd">

    ...

    <!-- Begin Web Security -->
    <!-- unsecured resource if needed -->
    <sec:http pattern="/images/**" security="none"/> 
    <sec:http>
        <!-- configure the roles allowed to access the app -->
        <sec:intercept-url pattern="/**" access="hasAnyRole('MANAGER', 'USER')"/>
        <!-- add more urls/patters/roles to refine security -->

        <sec:form-login/>
        <sec:logout/>
        <!-- if you are adding to an exiting app, 
                you may need to disable CSRF protection until you can make application changes. -->
        <!-- sec:csrf disabled="true"/ -->
    </sec:http>

    <!-- add the properties below to your app's properties file 
            or replace with hardcoded values to get working -->
    <sec:ldap-server 
        id="contextSource" 
        url="ldap://${ldap.server}:${ldap.port}/"
        manager-dn="${ldap.manager.user}"
        manager-password="${ldap.manager.password}"/>

    <sec:authentication-manager erase-credentials="true">
        <sec:authentication-provider ref='ldapAuthProvider' />
    </sec:authentication-manager>


    <!-- using bean-based configuration here to set the DefaultLdapAuthoritiesPopulater with 
        ignorePartialResultsException=true.  This is a known Spring/AD issue and a workaround for it -->
    <bean id="ldapAuthProvider" class="org.springframework.security.ldap.authentication.LdapAuthenticationProvider">
        <constructor-arg>
            <!-- the bind authenticator will first lookup the user using the service account credentials, then 
                 attempt to bind the user with their password once found -->
            <bean id="bindAuthenticator" class="org.springframework.security.ldap.authentication.BindAuthenticator">
                <constructor-arg ref="contextSource" />
                <property name="userSearch" ref="userSearch" />
            </bean>
        </constructor-arg>
        <constructor-arg>
            <bean class="org.springframework.security.ldap.userdetails.DefaultLdapAuthoritiesPopulator">
                <constructor-arg ref="contextSource" />
                <constructor-arg value="DC=company,DC=com" /> <!-- group search base -->
                <!-- <property name="defaultRole" value="ROLE_USER" /> 
                    You can add a default role to everyone if needed -->
                <property name="searchSubtree" value="true" />
                <property name="ignorePartialResultException" value="true" />
                <property name="groupSearchFilter" value="(member={0})" />
            </bean>
        </constructor-arg>
    </bean>
    <bean id="userSearch" class="org.springframework.security.ldap.search.FilterBasedLdapUserSearch">
        <constructor-arg index="0" value="DC=company,DC=com" />
        <constructor-arg index="1" value="(sAMAccountName={0})" />
        <constructor-arg index="2" ref="contextSource" />
        <property name="searchSubtree" value="true" />
    </bean>
    <!--  end Web Security -->
</beans>

以下就是我使用的属性信息。

# Ldap server access
ldap.server=LDAP01
ldap.port=389
ldap.manager.user=cn=LdapAuthUser,ou=ServiceAccounts,dc=company,dc=com
ldap.manager.password=SomePassword!

具体工作流程

当LdapAuthenticationProvider执行验证工作时,会做如下流程:
1. 通过设置来进行绑定LDAP服务器连接配置信息;
2. 通过userSearch配置并使用用户登录名查看/查询用户(进入到登录界面);
3. 匹配得到用户完整的distinguished name;
4. 通过输入的密码与LDAP再次进行校验正确性;
5. 通过groupSearchFilter配置搜索用户的所有组信息。

如果你设置root logger为debug,日志详情会打印出来(包括搜索的结果详情). 这是一个非常大的用处便于帮助我们分析配置信息。

为了确认你的LDAP配置都是准确的或用户密码错误问题,你可以使用LDAP browser(查看下面)来手动测试这些操作。

其他备注

  • 以上配置会自动使用/logout作为可用登出/注销方式,你应该在你的程序对此接口进行处理。
    想要从Controller或Service获取用户名可以使用如下方式:
SecurityContextHolder.getContext().getAuthentication().getName()