Spring MVC应用程序向Active Directory进行身份验证

时间:2014-05-12 19:17:45

标签: java spring spring-mvc spring-security active-directory

我意识到这个问题已经被提出,但我还没有找到符合我情况的任何其他帖子的解决方案。

我正在编写一个Spring MVC java应用程序,我试图让它对我们的Active Directory系统进行身份验证。我正在使用Spring Tools Suite 3.4.0并创建了一个Spring MVC项目。我正在使用Spring Security 3.1.1,一旦完成,该应用程序将被部署到在Linux上运行的Tomcat 7 java服务器上。

我的Active Directory系统中有一个用户,我知道其密码是有效的,因为我有其他应用程序可以对同一个Active Directory系统进行身份验证,我可以在其他应用程序中成功通过该用户进行身份验证。用户标识是:

myuser@mycompany.com

您可以看到存储在Active Directory系统中的用户ID采用电子邮件地址格式。

我们的Active Directory系统中的域控制器是:addomain.mycompany.com。域控制器下面是一个名为ExternalUsers的OU,这个OU有两个子OU,分别叫做Groups和Users。所以我的用户的路径是:

CN =为myuser @ mycompany.com,OU =用户,OU = ExternalUsers,DC = addomain,DC = myCompany中,DC = COM

拦截工作得很好......每当我尝试导航到应用程序中的任何网址时,我都会被重定向到登录页面。我遇到的麻烦就是验证。当我输入我的用户ID和密码并单击“提交”时,身份验证失败。这就是我在日志中看到的内容:

2014-05-12 08:42:32,916 DEBUG: org.springframework.security.ldap.authentication.ad.ActiveDirectoryLdapAuthenticationProvider - Processing authentication request for user: myuser@mycompany.com
2014-05-12 08:42:33,383 DEBUG: org.springframework.security.ldap.authentication.ad.ActiveDirectoryLdapAuthenticationProvider - Authentication for myuser@mycompany.com@addomain failed:
javax.naming.AuthenticationException: [LDAP: error code 49 - 80090308: LdapErr: DSID-0C0903A9, comment: AcceptSecurityContext error, data 52e, v1db1]
2014-05-12 08:42:33,384 INFO : org.springframework.security.ldap.authentication.ad.ActiveDirectoryLdapAuthenticationProvider - Active Directory authentication failed: Supplied password was invalid

我在下面的项目中列出了相关文件。

在spring-security-context.xml文件中,我尝试更改此行:

<beans:constructor-arg value="addomain" />

到此:

<beans:constructor-arg value="addomain.mycompany.com" />

不幸的是,我看到了同样的行为。日志与上面相同。

我也试过在spring-security-context.xml中切换以下行:

<security:intercept-url pattern="/**" access="ROLE_ADMIN" />

到此:

<security:intercept-url pattern="/**" access="ROLE_USER" />

但不幸的是我得到了相同的结果。我是否需要创建名称与角色名称匹配的Active Directory组(即ROLE_ADMIN或ROLE_USER)?

我发现另一篇文章似乎与我遇到的问题相符:

Spring ActiveDirectoryLdapAuthenticationProvider handleBindException - Supplied password was invalid error

不幸的是,它并没有真正提供解决方案。

我发现的一点是,我一直在使用的Active Directory用户的sAMAccountName值与userid不匹配。我创建了一个sAMAccountName匹配的新用户 用户ID,突然间我超过了&#34;提供的密码无效&#34;信息。但是,现在我在尝试登录时收到以下消息:

org.springframework.dao.IncorrectResultSizeDataAccessException:结果大小不正确:预期1,实际0

完整堆栈跟踪是:

2014-05-12 13:34:53,488 DEBUG: org.springframework.security.ldap.authentication.ad.ActiveDirectoryLdapAuthenticationProvider - Processing authentication request for user: myuser@mycompany.com
2014-05-12 13:34:53,578 INFO : org.springframework.security.ldap.SpringSecurityLdapTemplate - Ignoring PartialResultException
May 12, 2014 1:34:53 PM org.apache.catalina.core.StandardWrapperValve invoke
SEVERE: Servlet.service() for servlet [appServlet] in context with path [/pima] threw exception
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)
    at org.springframework.security.ldap.authentication.ad.ActiveDirectoryLdapAuthenticationProvider.doAuthentication(ActiveDirectoryLdapAuthenticationProvider.java:114)
    at org.springframework.security.ldap.authentication.AbstractLdapAuthenticationProvider.authenticate(AbstractLdapAuthenticationProvider.java:61)
    at org.springframework.security.authentication.ProviderManager.authenticate(ProviderManager.java:156)
    at org.springframework.security.authentication.ProviderManager.authenticate(ProviderManager.java:174)
    at org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter.attemptAuthentication(UsernamePasswordAuthenticationFilter.java:94)
    at org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter.doFilter(AbstractAuthenticationProcessingFilter.java:194)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:334)
    at org.springframework.security.web.authentication.logout.LogoutFilter.doFilter(LogoutFilter.java:105)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:334)
    at org.springframework.security.web.context.SecurityContextPersistenceFilter.doFilter(SecurityContextPersistenceFilter.java:87)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:334)
    at org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:184)
    at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:155)
    at org.springframework.web.filter.DelegatingFilterProxy.invokeDelegate(DelegatingFilterProxy.java:346)
    at org.springframework.web.filter.DelegatingFilterProxy.doFilter(DelegatingFilterProxy.java:259)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:243)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:210)
    at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:222)
    at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:123)
    at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:502)
    at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:171)
    at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:99)
    at org.apache.catalina.valves.AccessLogValve.invoke(AccessLogValve.java:953)
    at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:118)
    at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:408)
    at org.apache.coyote.http11.AbstractHttp11Processor.process(AbstractHttp11Processor.java:1023)
    at org.apache.coyote.AbstractProtocol$AbstractConnectionHandler.process(AbstractProtocol.java:589)
    at org.apache.tomcat.util.net.JIoEndpoint$SocketProcessor.run(JIoEndpoint.java:312)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:615)
    at java.lang.Thread.run(Thread.java:722)

我有点被困在这一点上,任何人都可以帮助我理解我在这里做错了什么和/或我需要做些什么才能让它发挥作用?

这是我的login.jsp文件:

<%@ page language="java" contentType="text/html; charset=ISO-8859-1" pageEncoding="ISO-8859-1"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1">
<title>Insert title here</title>
</head>
<body>
<form method="post" id="loginForm" action="<c:url value='/j_spring_security_check' >    </c:url>" >
     <table>
<tr>
            <td>
            UserId:
        </td>
        <td>
            <input type="text" size="50" name="j_username"></input>
        </td>
    </tr>
    <tr>
        <td>
            Password:
        </td>
        <td>
            <input type="password" name="j_password"></input>
        </td>
    </tr>
</table>
<br />

<input type="submit"></input>
</form>
</body>
</html>

这是我的web.xml文件:

<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.5" xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee     http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">

<!-- The definition of the Root Spring Container shared by all Servlets and Filters -->
<context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>/WEB-INF/spring/root-context.xml</param-value>
</context-param>

<!-- Creates the Spring Container shared by all Servlets and Filters -->
<listener>
    <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>

<!-- Processes application requests -->
<servlet>
    <servlet-name>appServlet</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <init-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>/WEB-INF/spring/appServlet/servlet-context.xml</param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>
</servlet>

<servlet-mapping>
    <servlet-name>appServlet</servlet-name>
    <url-pattern>/</url-pattern>
</servlet-mapping>

<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>
</web-app>

这是我的servlet-context.xml文件:

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

<!-- DispatcherServlet Context: defines this servlet's request-processing infrastructure -->

<!-- Enables the Spring MVC @Controller programming model -->
<annotation-driven />

<!-- Handles HTTP GET requests for /resources/** by efficiently serving up static resources in the ${webappRoot}/resources directory -->
<resources mapping="/resources/**" location="/resources/" />

<!-- Resolves views selected for rendering by @Controllers to .jsp resources in the /WEB-INF/views directory -->
<beans:bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
    <beans:property name="prefix" value="/WEB-INF/views/" />
    <beans:property name="suffix" value=".jsp" />
</beans:bean>

<context:component-scan base-package="com.mycompany.pima" />

<!-- Create DataSource Bean for connection to the SQL Server database -->
<beans:bean id="dbDataSource"
    class="org.springframework.jndi.JndiObjectFactoryBean">
    <beans:property name="jndiName" value="java:comp/env/jdbc/MyDB"/>
</beans:bean>

<beans:bean id="adSettings"
    class="org.springframework.jndi.JndiObjectFactoryBean">
    <beans:property name="jndiName" value="java:comp/env/adSettings"/>
</beans:bean>

<beans:bean id="portalSettings"
    class="org.springframework.jndi.JndiObjectFactoryBean">
    <beans:property name="jndiName" value="java:comp/env/portalSettings"/>
</beans:bean>

</beans:beans>

这是我的root-context.xml文件:

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

<!-- Root Context: defines shared resources visible to all other web components -->

<import resource="spring-security-context.xml"/>

</beans>

这是我的spring-security-context.xml文件:

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

<security:http pattern="/login" security="none" />
<security:http pattern="/logerror" security="none" />

<!-- LDAP server details -->
<security:authentication-manager>
    <security:authentication-provider ref="ldapActiveDirectoryAuthProvider" />
</security:authentication-manager>

<beans:bean id="grantedAuthoritiesMapper" class="com.mycompany.pima.security.ActiveDirectoryGrantedAuthoritiesMapper"/>

<beans:bean id="ldapActiveDirectoryAuthProvider" class="org.springframework.security.ldap.authentication.ad.ActiveDirectoryLdapAuthenticationProvider">
    <beans:constructor-arg value="addomain" />
    <beans:constructor-arg value="ldap://dev_ad_system.addomain.mycompany.com:389/" />
    <beans:property name="authoritiesMapper" ref="grantedAuthoritiesMapper" />
    <beans:property name="useAuthenticationRequestCredentials" value="true" />
    <beans:property name="convertSubErrorCodesToExceptions" value="true" />
</beans:bean>

<security:http auto-config="true" pattern="/**">
    <!-- Login pages -->
    <security:form-login login-page="/login" default-target-url="/users"
        login-processing-url="/j_spring_security_check" authentication-failure-url="/login?error=true" />
    <security:logout logout-success-url="/login"/>

       <!-- Security zones -->
    <security:intercept-url pattern="/**" access="ROLE_ADMIN" />

</security:http>

</beans:beans>

最后,这是我的pom.xml文件:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.mycompany</groupId>
<artifactId>pima</artifactId>
<name>PIMA</name>
<packaging>war</packaging>
<version>1.0.0-BUILD-SNAPSHOT</version>
<properties>
    <java-version>1.6</java-version>
    <org.springframework-version>3.1.1.RELEASE</org.springframework-version>
    <spring.security.version>3.1.1.RELEASE</spring.security.version>
    <org.aspectj-version>1.6.10</org.aspectj-version>
    <org.slf4j-version>1.6.6</org.slf4j-version>
</properties>
<dependencies>
    <!-- Spring -->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>${org.springframework-version}</version>
        <exclusions>
            <!-- Exclude Commons Logging in favor of SLF4j -->
            <exclusion>
                <groupId>commons-logging</groupId>
                <artifactId>commons-logging</artifactId>
             </exclusion>
        </exclusions>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-webmvc</artifactId>
        <version>${org.springframework-version}</version>
    </dependency>

    <!-- AspectJ -->
    <dependency>
        <groupId>org.aspectj</groupId>
        <artifactId>aspectjrt</artifactId>
        <version>${org.aspectj-version}</version>
    </dependency>   

    <!-- Logging -->
    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>slf4j-api</artifactId>
        <version>${org.slf4j-version}</version>
    </dependency>
    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>jcl-over-slf4j</artifactId>
        <version>${org.slf4j-version}</version>
        <scope>runtime</scope>
    </dependency>
    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>slf4j-log4j12</artifactId>
        <version>${org.slf4j-version}</version>
        <scope>runtime</scope>
    </dependency>
    <dependency>
        <groupId>log4j</groupId>
        <artifactId>log4j</artifactId>
        <version>1.2.15</version>
        <exclusions>
            <exclusion>
                <groupId>javax.mail</groupId>
                <artifactId>mail</artifactId>
            </exclusion>
            <exclusion>
                <groupId>javax.jms</groupId>
                <artifactId>jms</artifactId>
            </exclusion>
            <exclusion>
                <groupId>com.sun.jdmk</groupId>
                <artifactId>jmxtools</artifactId>
            </exclusion>
            <exclusion>
                <groupId>com.sun.jmx</groupId>
                <artifactId>jmxri</artifactId>
            </exclusion>
        </exclusions>
        <scope>runtime</scope>
    </dependency>

    <!-- @Inject -->
    <dependency>
        <groupId>javax.inject</groupId>
        <artifactId>javax.inject</artifactId>
        <version>1</version>
    </dependency>

    <!-- Servlet -->
    <dependency>
        <groupId>javax.servlet</groupId>
        <artifactId>servlet-api</artifactId>
        <version>2.5</version>
        <scope>provided</scope>
    </dependency>
    <dependency>
        <groupId>javax.servlet.jsp</groupId>
        <artifactId>jsp-api</artifactId>
        <version>2.1</version>
        <scope>provided</scope>
    </dependency>
    <dependency>
        <groupId>javax.servlet</groupId>
        <artifactId>jstl</artifactId>
        <version>1.2</version>
    </dependency>

    <!-- Test -->
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.7</version>
        <scope>test</scope>
    </dependency>        

    <dependency>
        <groupId>org.springframework.security</groupId>
        <artifactId>spring-security-core</artifactId>
        <version>${spring.security.version}</version>
    </dependency>
    <dependency>
        <groupId>org.springframework.security</groupId>
        <artifactId>spring-security-web</artifactId>
        <version>${spring.security.version}</version>
    </dependency>
    <dependency>
        <groupId>org.springframework.security</groupId>
        <artifactId>spring-security-config</artifactId>
        <version>${spring.security.version}</version>
    </dependency>
    <dependency>
        <groupId>org.springframework.security</groupId>
        <artifactId>spring-security-taglibs</artifactId>
        <version>${spring.security.version}</version>
    </dependency>
    <dependency>
        <groupId>org.springframework.security</groupId>
        <artifactId>spring-security-ldap</artifactId>
        <version>${spring.security.version}</version>
    </dependency>
    <dependency>
        <groupId>org.springframework.ldap</groupId>
        <artifactId>spring-ldap-core</artifactId>
        <version>1.3.1.RELEASE</version>
    </dependency>
    <dependency>
        <groupId>net.sourceforge.jtds</groupId>
        <artifactId>jtds</artifactId>
        <version>1.3.1</version>
    </dependency>
    <dependency>
        <groupId>commons-dbcp</groupId>
        <artifactId>commons-dbcp</artifactId>
        <version>1.4</version>
    </dependency>
</dependencies>
<build>
    <plugins>
        <plugin>
            <artifactId>maven-eclipse-plugin</artifactId>
            <version>2.9</version>
            <configuration>
                <additionalProjectnatures>
                    <projectnature>org.springframework.ide.eclipse.core.springnature</projectnature>
                </additionalProjectnatures>
                <additionalBuildcommands>
                    <buildcommand>org.springframework.ide.eclipse.core.springbuilder</buildcommand>
                </additionalBuildcommands>
                <downloadSources>true</downloadSources>
                <downloadJavadocs>true</downloadJavadocs>
            </configuration>
        </plugin>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <version>2.5.1</version>
            <configuration>
                <source>1.6</source>
                <target>1.6</target>
                <compilerArgument>-Xlint:all</compilerArgument>
                <showWarnings>true</showWarnings>
                <showDeprecation>true</showDeprecation>
            </configuration>
        </plugin>
        <plugin>
            <groupId>org.codehaus.mojo</groupId>
            <artifactId>exec-maven-plugin</artifactId>
            <version>1.2.1</version>
            <configuration>
                <mainClass>org.test.int1.Main</mainClass>
            </configuration>
        </plugin>
    </plugins>
</build>
</project>

谢谢,

-Stephen Spalding

1 个答案:

答案 0 :(得分:7)

更新......我相信我已经解决了我的问题。

首先,我正在处理的原始问题是我在尝试使用已知良好的用户ID /密码登录时收到以下消息:

org.springframework.security.ldap.authentication.ad.ActiveDirectoryLdapAuthenticationProvider - Active Directory authentication failed: Supplied password was invalid

我可以确认,一旦我创建了一个在Active Directory中的userid和sAMAccountName值相同的新用户,此问题就解决了。认证片 直到我这样做并且开始与该用户一起测试之后才工作。

我处理的第二个问题是在我开始使用具有匹配的userid和sAMAccountNames的用户之后。我在尝试登录时开始收到以下消息:

org.springframework.dao.IncorrectResultSizeDataAccessException: Incorrect result size: expected 1, actual 0

我最终实现了自己的自定义身份验证提供程序,并且必须对其进行一些调整才能使其与我们的Active Directory系统一起使用。我已经发布了每个班级 我在下面创建的文件。

最后,必须存在与ActiveDirectoryGrantedAuthoritiesMapper类中的常量值匹配的Active Directory组,并且您的用户必须包含在此组中。 在该文件中查看此行:

private String ROLE_ADMIN = "ExtranetUsers";

我还没有遇到过更多需要解决的问题,但我可以说我现在通过Active Directory成功登录到Spring应用程序。

我将我为自定义身份验证提供程序创建的所有类文件放在名为com.mycompany.pima.security的包中。请注意实现此自定义 身份验证提供程序是我在login.jsp,web.xml,servlet-context.xml,root-context.xml,spring-security-context.xml和pom.xml中的配置之外所做的。 com.mycompany.pima.security包中的文件是:

ActiveDirectoryLdapAuthenticationProvider.java :(请参阅我在searchForUser()方法中的注释,了解我最终工作的内容)

package com.mycompany.pima.security;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.dao.IncorrectResultSizeDataAccessException;
import org.springframework.ldap.core.DirContextOperations;
import org.springframework.ldap.core.DistinguishedName;
import org.springframework.ldap.core.support.DefaultDirObjectFactory;
import org.springframework.ldap.support.LdapUtils;
import org.springframework.security.authentication.AccountExpiredException;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.CredentialsExpiredException;
import org.springframework.security.authentication.DisabledException;
import org.springframework.security.authentication.LockedException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.ldap.SpringSecurityLdapTemplate;
import     org.springframework.security.ldap.authentication.AbstractLdapAuthenticationProvider;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;

import javax.naming.AuthenticationException;
import javax.naming.Context;
import javax.naming.NamingException;
import javax.naming.OperationNotSupportedException;
import javax.naming.directory.DirContext;
import javax.naming.directory.SearchControls;
import javax.naming.ldap.InitialLdapContext;

import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public final class ActiveDirectoryLdapAuthenticationProvider extends AbstractLdapAuthenticationProvider {
private static final Pattern SUB_ERROR_CODE = Pattern.compile(".*data\\s([0-9a-f]{3,4}).*");

// Error codes
private static final int USERNAME_NOT_FOUND = 0x525;
private static final int INVALID_PASSWORD = 0x52e;
private static final int NOT_PERMITTED = 0x530;
private static final int PASSWORD_EXPIRED = 0x532;
private static final int ACCOUNT_DISABLED = 0x533;
private static final int ACCOUNT_EXPIRED = 0x701;
private static final int PASSWORD_NEEDS_RESET = 0x773;
private static final int ACCOUNT_LOCKED = 0x775;

private final String domain;
private final String rootDn;
private final String url;
private boolean convertSubErrorCodesToExceptions;

private static final Logger logger = LoggerFactory.getLogger(ActiveDirectoryLdapAuthenticationProvider.class);

// Only used to allow tests to substitute a mock LdapContext
ContextFactory contextFactory = new ContextFactory();

/**
 * @param domain the domain for which authentication should take place
 */
//    public ActiveDirectoryLdapAuthenticationProvider(String domain) {
//        this (domain, null);
//    }

/**
 * @param domain the domain name (may be null or empty)
 * @param url an LDAP url (or multiple URLs)
 */
public ActiveDirectoryLdapAuthenticationProvider(String domain, String url) {

    Assert.isTrue(StringUtils.hasText(url), "Url cannot be empty");
    this.domain = StringUtils.hasText(domain) ? domain.toLowerCase() : null;
    //this.url = StringUtils.hasText(url) ? url : null;
    this.url = url;
    rootDn = this.domain == null ? null : rootDnFromDomain(this.domain);
}

@Override
protected DirContextOperations doAuthentication(UsernamePasswordAuthenticationToken auth) {

    String username = auth.getName();
    String password = (String)auth.getCredentials();
    DirContext ctx = bindAsUser(username, password);

    try {
        return searchForUser(ctx, username);

    } catch (NamingException e) {
        logger.error("Failed to locate directory entry for authenticated user: " + username, e);
        throw badCredentials(e);
    } finally {
        LdapUtils.closeContext(ctx);
    }
}

/**
 * Creates the user authority list from the values of the {@code memberOf} attribute obtained from the user's
 * Active Directory entry.
 */
@Override
protected Collection<? extends GrantedAuthority> loadUserAuthorities(DirContextOperations userData, String username, String password) {
    String[] groups = userData.getStringAttributes("memberOf");

    if (groups == null) {
        logger.debug("No values for 'memberOf' attribute.");
        return AuthorityUtils.NO_AUTHORITIES;
    }

    if (logger.isDebugEnabled()) {
        logger.debug("'memberOf' attribute values: " + Arrays.asList(groups));
    }

    ArrayList<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>(groups.length);

    for (String group : groups) {
        authorities.add(new SimpleGrantedAuthority(new DistinguishedName(group).removeLast().getValue()));
    }

    return authorities;
}

private DirContext bindAsUser(String username, String password) {
    // TODO. add DNS lookup based on domain
    final String bindUrl = url;

    Hashtable<String,String> env = new Hashtable<String,String>();
    env.put(Context.SECURITY_AUTHENTICATION, "simple");

    String bindPrincipal = createBindPrincipal(username);

    env.put(Context.SECURITY_PRINCIPAL, bindPrincipal);
    env.put(Context.PROVIDER_URL, bindUrl);
    env.put(Context.SECURITY_CREDENTIALS, password);
    env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
    env.put(Context.OBJECT_FACTORIES, DefaultDirObjectFactory.class.getName());

    try {
        return contextFactory.createContext(env);
    } catch (NamingException e) {
        if ((e instanceof AuthenticationException) || (e instanceof OperationNotSupportedException)) {
            handleBindException(bindPrincipal, e);
            throw badCredentials(e);
        } else {
            throw LdapUtils.convertLdapException(e);
        }
    }
}

void handleBindException(String bindPrincipal, NamingException exception) {

    if (logger.isDebugEnabled()) {
        logger.debug("Authentication for " + bindPrincipal + " failed:" + exception);
    }

    int subErrorCode = parseSubErrorCode(exception.getMessage());

    if (subErrorCode > 0) {
        logger.info("Active Directory authentication failed: " + subCodeToLogMessage(subErrorCode));

        if (convertSubErrorCodesToExceptions) {
            raiseExceptionForErrorCode(subErrorCode, exception);
        }
    } else {
        logger.debug("Failed to locate AD-specific sub-error code in message");
    }
}

int parseSubErrorCode(String message) {
    Matcher m = SUB_ERROR_CODE.matcher(message);

    if (m.matches()) {
        return Integer.parseInt(m.group(1), 16);
    }

    return -1;
}

void raiseExceptionForErrorCode(int code, NamingException exception) {

    String hexString = Integer.toHexString(code);
    Throwable cause = new ActiveDirectoryAuthenticationException(hexString, exception.getMessage(), exception);
    switch (code) {
        case PASSWORD_EXPIRED:
            throw new CredentialsExpiredException(messages.getMessage("LdapAuthenticationProvider.credentialsExpired",
                    "User credentials have expired"), cause);
        case ACCOUNT_DISABLED:
            throw new DisabledException(messages.getMessage("LdapAuthenticationProvider.disabled",
                    "User is disabled"), cause);
        case ACCOUNT_EXPIRED:
            throw new AccountExpiredException(messages.getMessage("LdapAuthenticationProvider.expired",
                    "User account has expired"), cause);
        case ACCOUNT_LOCKED:
            throw new LockedException(messages.getMessage("LdapAuthenticationProvider.locked",
                    "User account is locked"), cause);
        default:
            throw badCredentials(cause);
    }
}

String subCodeToLogMessage(int code) {
    switch (code) {
        case USERNAME_NOT_FOUND:
            return "User was not found in directory";
        case INVALID_PASSWORD:
            return "Supplied password was invalid";
        case NOT_PERMITTED:
            return "User not permitted to logon at this time";
        case PASSWORD_EXPIRED:
            return "Password has expired";
        case ACCOUNT_DISABLED:
            return "Account is disabled";
        case ACCOUNT_EXPIRED:
            return "Account expired";
        case PASSWORD_NEEDS_RESET:
            return "User must reset password";
        case ACCOUNT_LOCKED:
            return "Account locked";
    }

    return "Unknown (error code " + Integer.toHexString(code) +")";
}

private BadCredentialsException badCredentials() {
    return new BadCredentialsException(messages.getMessage(
                    "LdapAuthenticationProvider.badCredentials", "Bad credentials"));
}

private BadCredentialsException badCredentials(Throwable cause) {
    return (BadCredentialsException) badCredentials().initCause(cause);
}

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

    // This was the original setting for searchFilter:
    // String searchFilter = "(&(objectClass=user)(userPrincipalName={0}))";

    // These are some of the values that I played around with:
    // String searchFilter = "(&(objectClass=user)(|(userPrincipalName={0})(sAMAccountName={1})))";
    // String searchFilter = "(&(objectClass=user)((sAMAccountName={1})))";
    // String searchFilter = "(&(sAMAccountName=" + username + "))";

    // This is the final searchFilter value that I used that actually worked:
    String searchFilter = "(&(cn=" + username + "))";

    final String bindPrincipal = createBindPrincipal(username);

    String searchRoot = rootDn != null ? rootDn : searchRootFromPrincipal(bindPrincipal);
    // This is also something that I had to add to match my OU path:
    searchRoot = "ou=ExternalUsers," + searchRoot;

    try {
        return SpringSecurityLdapTemplate.searchForSingleEntryInternal(ctx, searchCtls, searchRoot, searchFilter,
            new Object[]{bindPrincipal});
    } catch (IncorrectResultSizeDataAccessException incorrectResults) {
        if (incorrectResults.getActualSize() == 0) {
            UsernameNotFoundException userNameNotFoundException = new UsernameNotFoundException("User " + username + " not found in directory.", username);
            userNameNotFoundException.initCause(incorrectResults);
            throw badCredentials(userNameNotFoundException);
        }
        // Search should never return multiple results if properly configured, so just rethrow
        throw incorrectResults;
    }
}

private String searchRootFromPrincipal(String bindPrincipal) {
    int atChar = bindPrincipal.lastIndexOf('@');

    if (atChar < 0) {
        logger.debug("User principal '" + bindPrincipal + "' does not contain the domain, and no domain has been configured");
        throw badCredentials();
    }

    return rootDnFromDomain(bindPrincipal.substring(atChar+ 1, bindPrincipal.length()));
}

private String rootDnFromDomain(String domain) {
    String[] tokens = StringUtils.tokenizeToStringArray(domain, ".");
    StringBuilder root = new StringBuilder();

    for (String token : tokens) {
        if (root.length() > 0) {
            root.append(',');
        }
        root.append("dc=").append(token);
    }

    return root.toString();
}

String createBindPrincipal(String username) {
    if (domain == null || username.toLowerCase().endsWith(domain)) {
        return username;
    }
    return username + "@" + domain;
}

/**
 * By default, a failed authentication (LDAP error 49) will result in a {@code BadCredentialsException}.
 * <p>
 * If this property is set to {@code true}, the exception message from a failed bind attempt will be parsed
 * for the AD-specific error code and a {@link CredentialsExpiredException}, {@link DisabledException},
 * {@link AccountExpiredException} or {@link LockedException} will be thrown for the corresponding codes. All
 * other codes will result in the default {@code BadCredentialsException}.
 *
 * @param convertSubErrorCodesToExceptions {@code true} to raise an exception based on the AD error code.
 */
public void setConvertSubErrorCodesToExceptions(boolean convertSubErrorCodesToExceptions) {
    this.convertSubErrorCodesToExceptions = convertSubErrorCodesToExceptions;
}

static class ContextFactory {
    DirContext createContext(Hashtable<?,?> env) throws NamingException {
        return new InitialLdapContext(env, null);
    }
}
}

ActiveDirectoryGrantedAuthoritiesMapper.java :(请参阅我对于给予ROLE_ADMIN的值必须存在的Active Directory组的评论。另外 ROLE_ADMIN 必须 在security:intercept-url标记中的spring-security-context.xml文件中引用

package com.mycompany.pima.security;

import java.util.Collection;
import java.util.EnumSet;
import java.util.Set;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;

public class ActiveDirectoryGrantedAuthoritiesMapper implements GrantedAuthoritiesMapper {

// Constants for group defined in LDAP
// The string "ExtranetUsers" in the ROLE_ADMIN var below actually maps to the name
// of an Active Directory group.
private String ROLE_ADMIN = "ExtranetUsers";

private static final Logger logger = LoggerFactory.getLogger(ActiveDirectoryGrantedAuthoritiesMapper.class);

public ActiveDirectoryGrantedAuthoritiesMapper() {

}

@Override
public Collection<? extends GrantedAuthority> mapAuthorities(
        final Collection<? extends GrantedAuthority> authorities) {

    Set<SecurityContextAuthority> roles = EnumSet.noneOf(SecurityContextAuthority.class);

    for (GrantedAuthority authority : authorities) {

        // authority.getAuthority() returns the role in LDAP nomenclature
        if (ROLE_ADMIN.equals(authority.getAuthority())) {
            roles.add(SecurityContextAuthority.ROLE_ADMIN);
        }
    }
    return roles;
}
 }

ActiveDirectoryAuthenticationException.java:

package com.mycompany.pima.security;

import org.springframework.security.core.AuthenticationException;

@SuppressWarnings("serial")
public final class ActiveDirectoryAuthenticationException extends AuthenticationException {
private final String dataCode;

ActiveDirectoryAuthenticationException(String dataCode, String message, Throwable cause) {
    super(message, cause);
    this.dataCode = dataCode;
}

public String getDataCode() {
    return dataCode;
}
}

SecurityContextAuthority.java:

package com.mycompany.pima.security;

import org.springframework.security.core.GrantedAuthority;

public enum SecurityContextAuthority implements GrantedAuthority {

// These roles are specified in the security context (security.xml) and are
// mapped to LDAP roles by the ActiveDirectoryGrantedAuthoritiesMapper
ROLE_ADMIN;

@Override
public String getAuthority() {
    return name();
}   
}

然后我不得不调整spring-security-context.xml文件。现在看来是这样的:

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

<security:http pattern="/login" security="none" />
<security:http pattern="/logerror" security="none" />

<!-- LDAP server details -->
<security:authentication-manager>
    <security:authentication-provider ref="ldapActiveDirectoryAuthProvider" />
</security:authentication-manager>

<beans:bean id="grantedAuthoritiesMapper" class="com.mycompany.pima.security.ActiveDirectoryGrantedAuthoritiesMapper"/>

<beans:bean id="ldapActiveDirectoryAuthProvider" class="org.springframework.security.ldap.authentication.ad.ActiveDirectoryLdapAuthenticationProvider">
    <beans:constructor-arg value="addomain.mycompany.com" />
    <beans:constructor-arg value="ldap://dev_ad_system.addomain.mycompany.com:389/" />
    <beans:property name="authoritiesMapper" ref="grantedAuthoritiesMapper" />
    <beans:property name="useAuthenticationRequestCredentials" value="true" />
    <beans:property name="convertSubErrorCodesToExceptions" value="true" />
</beans:bean>

<security:http auto-config="true" pattern="/**">
    <!-- Login pages -->
    <security:form-login login-page="/login" default-target-url="/users"
        login-processing-url="/j_spring_security_check" authentication-failure-url="/login?error=true" />
    <security:logout logout-success-url="/login"/>

       <!-- Security zones -->
       <!-- ROLE_ADMIN mentioned in the line below must match the name of the constant in the ActiveDirectoryGrantedAuthoritiesMapper.java file. -->
    <security:intercept-url pattern="/**" access="ROLE_ADMIN" />

</security:http>

</beans:beans>

我希望这对某人有所帮助,因为我知道这可能是多么复杂和令人沮丧。

谢谢!

-Stephen Spalding