[英]Issue while Authenticating user with Active Directory (using AD domain) using Spring Security from Spring-boot Application
[英]Spring MVC application authenticating to 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系統中的用戶標識采用電子郵件地址的格式。
我們的Active Directory系統中的域控制器是:addomain.mycompany.com。 域控制器下面是一個名為ExternalUsers的OU,這個OU有兩個子OU,分別叫做Groups和Users。 所以我的用戶的路徑是:
CN =為myuser @ mycompany.com,OU =用戶,OU = ExternalUsers,DC = addomain,DC = myCompany中,DC = com的
攔截工作得很好......每當我嘗試導航到應用程序中的任何URL時,我都會被重定向到登錄頁面。 我遇到的麻煩是驗證。 當我輸入我的用戶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 - 提供的密碼無效錯誤
不幸的是,它並沒有真正提供解決方案。
我發現的一點是,我一直在使用的Active Directory用戶的sAMAccountName值與userid不匹配。 我創建了一個新用戶,其sAMAccountName與用戶ID匹配,突然我超過了“提供的密碼無效”消息。 但是,現在我在嘗試登錄時收到以下消息:
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>
謝謝,
- 斯蒂芬斯伯丁
更新......我相信我已經解決了我的問題。
首先,我正在處理的原始問題是我在嘗試使用已知良好的用戶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組的評論。還必須在security:intercept-url標記的spring-security-context.xml文件中引用ROLE_ADMIN)
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>
我希望這有助於某人,因為我知道這可能是多么復雜和令人沮喪。
謝謝!
- 斯蒂芬斯伯丁
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.