我正在通过Spring Tools Suite 3.4使用带有Spring Security 3.1.1的Spring MVC开发一个新的Web应用程序。我的应用程序是用java 1.6编写的,它是针对Active Directory系统进行身份验证的,它将部署到Tomcat 7服务器上。
我将通过WAR文件将应用程序部署到三个不同的环境:dev,qa和prod。我通常对每个环境唯一的设置所做的事情,例如数据库连接字符串(每个环境都有一个单独的数据库),配置Tomcat服务器的context.xml文件,通过jndi查找在我的Spring应用程序中,将这些设置注入我的DAO类。我现在面临的挑战是弄清楚如何为需要注入spring-security-context.xml文件的Active Directory设置执行类似操作。
截至目前,我已经在我的spring-security-context.xml文件中获取了我的Active Directory域和url硬编码,但是我不想这样做,因为有不同的适用于我的每个环境的Active Directory系统。我想这让我感到困惑的是我从spring-security-context.xml文件中将构造函数和属性值注入到我的ActiveDirectoryLdapAuthenticationProvider类中,但是如何将这些设置注入spring-security我的Tomcat服务器的context.xml文件中的-context.xml文件?
这是我的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.1.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" />
<security:http pattern="/resources/**" 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="com.mycompany.pima.security.ActiveDirectoryLdapAuthenticationProvider">
<beans:constructor-arg value="mydomain.mycompany.com" />
<beans:constructor-arg value="ldap://adserver.mydomain.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_USERS" />
<security:intercept-url pattern="/admin/**"
access="ROLE_ADMIN" />
<security:session-management
invalid-session-url="/login">
<security:concurrency-control
max-sessions="1" expired-url="/login" />
</security:session-management>
</security:http>
</beans:beans>
这是我的自定义ActiveDirectoryLdapAuthenticationProvider.java文件:
package com.mycompany.pima.security;
imports...
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();
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 new InitialDirContext(env);
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) {
logger.info("in parseSubErrorCode");
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);
String searchFilter = "(&(cn=" + username + "))";
final String bindPrincipal = createBindPrincipal(username);
String searchRoot = rootDn != null ? rootDn : searchRootFromPrincipal(bindPrincipal);
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)) {
logger.info("in createBindPrincipal: in the if, username = " + username);
return username;
}
// return username + "@" + domain;
return username;
}
/**
* 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);
}
}
}
这是我的本地主机Tomcat服务器(实际上,它是VMWare vFabric tc服务器)context.xml文件:
<?xml version="1.0" encoding="UTF-8"?>
<Context reloadable="true" docBase="myApp" path="/myApp"
source="org.eclipse.jst.jee.server:app">
<!-- Default set of monitored resources -->
<WatchedResource>WEB-INF/web.xml</WatchedResource>
<!-- Uncomment this to disable session persistence across Tomcat restarts -->
<!-- <Manager pathname="" /> -->
<!-- Uncomment this to enable Comet connection tacking (provides events
on session expiration as well as webapp lifecycle) -->
<!-- <Valve className="org.apache.catalina.valves.CometConnectionManagerValve"
/> -->
<Resource name="jdbc/MyDB" auth="Container" type="javax.sql.DataSource"
driverClassName="net.sourceforge.jtds.jdbc.Driver"
url="jdbc:jtds:sqlserver://dbserver:1433/MyInstance;instance=dev"
username="dbuserid" password="dbpassword" />
</Context>
任何人都可以帮助我了解如何将context.xml域和url设置从context.xml文件注入spring-security-context.xml 文件?
修改
domain和url是我需要从context.xml注入的两个值。但是,如果我要注入价值 从context.xml文件中,我想我需要包含ldapActiveDirectoryAuthProvider所需的所有值 类。我认为我需要添加的context.xml文件中的条目是这样的:
<Resource name="ldapAdAuthProviderSettings" auth="Container" type="com.mycompany.pima.ActiveDirectoryLdapAuthenticationProvider"
domain="mydomain.mycompany.com"
url="ldap://adserver.mydomain.mycompany.com:389/"
authoritiesMapper="com.mycompany.pima.security.ActiveDirectoryGrantedAuthoritiesMapper"
useAuthenticationRequestCredentials="true"
convertSubErrorCodesToExceptions="true"
/>
然后在我的spring-security-context.xml文件中,我需要将我的ldapActiveDirectoryAuthProvider条目调整为:
<beans:bean id="ldapActiveDirectoryAuthProvider"
class="org.springframework.jndi.JndiObjectFactoryBean">
<beans:property name="jndiName" value="java:comp/env/ldapAdAuthProviderSettings"/>
</beans:bean>
当我尝试此配置时,出现以下错误:
2014-05-22 13:23:39,219错误:org.springframework.web.context.ContextLoader - 上下文初始化失败 org.springframework.beans.factory.BeanCreationException:使用名称&#39; org.springframework.security.filterChains&#39;创建bean时出错:无法解析对bean的引用&org.springframework.security.web.DefaultSecurityFilterChain#3& #39;
谢谢!
-StephenS
答案 0 :(得分:1)
我已经解决了我的问题,虽然解决方案让我走的路线不同于我最初尝试实施的路线。我的最终目标是不对Active Directory服务器进行硬编码 我的spring-security-context.xml文件中的域和URL,但是要读入并注入来自我的Tomcat服务器上的外部源的值。我希望能够创建一个war文件 对于我的Spring应用程序并将其从一个Tomcat服务器移动到另一个服务器并使其连接到相应的Active Directory环境而无需任何手动干预。
我最初尝试通过将Active Directory设置添加为“资源”来实现此目标。在Tomcat的context.xml文件中,然后使用我的代码中的JNDI读取它。我拿了 这种方法,因为它是我之前为数据库连接成功完成的,以及每个Tomcat服务器独有的其他设置。我试过几个不同的 spring-security-context.xml,servlet-context.xml和context.xml文件中的设置组合,但从未能使其工作。
我读到了在Spring项目的目录结构中创建包含我的变量的属性文件,然后在代码中使用属性占位符。这个想法就是这样 一旦在Tomcat服务器上构建,部署和爆炸war文件,我就可以替换属性文件的内容。我的项目中属性文件的位置 需要包含在项目的类路径中。可以使用的一些文件夹是WEB-INF / classes或WEB-INF / lib。虽然这个想法对我有用,但我发现它有点像 在部署/爆炸war文件并用适当的设置替换属性文件的内容之后,必须记住要记住登录每个单独的Tomcat服务器。
我最终做的是在Tomcat的目录结构中创建一个新文件夹,将其包含在Tomcat的类路径中,然后将我的属性文件放在那里。我能够成功使用 我的spring-security-context.xml文件中的属性占位符从我的属性文件中更新。完成这项工作需要花费一些时间,因为我必须修改它 catalina.properties文件并找出需要去的地方。
在我的Spring Tools Suite IDE中,catalina.properties文件的位置为:
C:\eclipse\springsource\vfabric-tc-server-developer-2.9.3.RELEASE\base-instance\conf\catalina.properties
在此文件中,我更改了以下行:
shared.loader=
到此:
shared.loader=\
${catalina.home}/shared/lib
我将更改保存到catalina.properties文件中,然后创建了&#39; \ shared \ lib&#39;以下文件夹中的文件夹(试图保持常规):
C:\eclipse\springsource\vfabric-tc-server-developer-2.9.3.RELEASE\tomcat-7.0.42.A.RELEASE
然后我将我的属性文件ExternaActiveDirectory.properties放在此文件夹中。所以我的属性文件的完整路径是:
C:\eclipse\springsource\vfabric-tc-server-developer-2.9.3.RELEASE\tomcat-7.0.42.A.RELEASE\shared\lib\ExternalActiveDirectory.properties
我的ExternalActiveDirectory.properties文件的内容是:
ldap.domain=mydomain.mycompany.com
ldap.url=ldap://adserver.mydomain.mycompany.com:389/
我将spring-security-context.xml文件的ldapActiveDirectoryAuthProvider bean更改为如下所示:
<beans:bean id="ldapActiveDirectoryAuthProvider" class="com.graybar.pima.security.ActiveDirectoryLdapAuthenticationProvider">
<beans:constructor-arg value="${ldap.domain}" />
<beans:constructor-arg value="${ldap.url}" />
<beans:property name="authoritiesMapper" ref="grantedAuthoritiesMapper" />
<beans:property name="useAuthenticationRequestCredentials" value="true" />
<beans:property name="convertSubErrorCodesToExceptions" value="true" />
</beans:bean>
我还在spring-security-context.xml文件中包含了这一额外的配置:
<beans:bean id="activeDirectoryProperties"
class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
<beans:property name="location" value="classpath:ExternalActiveDirectory.properties" />
</beans:bean>
我停止并重新启动了我的localhost Tomcat服务器,它工作正常!我将ExternalActiveDirectory.properties文件的内容切换到另一台AD服务器,停止/重新启动tomcat,然后再次尝试,以确保它确实正常工作,并继续工作。
当我在Linux服务器上实现我的更改时,我对catalina.properties文件中的shared.loader行进行了相同的更改,但是它的行号与Tomcat的localhost副本不同。此外,由于linux服务器上的$ CATALINA_HOME位置是/ opt / tomcat,因此我的属性文件具有以下路径:
/opt/tomcat/shared/lib/ExternalActiveDirectory.properties
我遵循的一些链接帮助了我:
http://www.mulesoft.com/tcat/tomcat-classpath Where to place and how to read configuration resource files in servlet based application? Tomcat 6 vs 7 - lib vs shared/lib - jars only?
我希望这可以帮助其他人!
-StephenS
答案 1 :(得分:0)
它只是一种正常的可注射资源,因此可以像其他任何方式一样注入,单向:
Resource(name="ldapActiveDirectoryAuthProvider")
ActiveDirectoryLdapAuthenticationProvider myProvider;
或者你可以自动装配。无论哪种方式,您都可以正常访问其属性和字段。