如何从 context.xml 注入值
Posted
技术标签:
【中文标题】如何从 context.xml 注入值【英文标题】:How to inject values from context.xml 【发布时间】:2014-07-11 15:47:03 【问题描述】:我正在通过 Spring Tools Suite 3.4 使用 Spring MVC 和 Spring Security 3.1.1 开发一个新的 Web 应用程序。我的应用程序是用 java 1.6 编写的,它针对 Active Directory 系统进行身份验证,并将部署到 Tomcat 7 服务器。
我将通过 WAR 文件将应用程序部署到三个不同的环境:dev、qa 和 prod。对于每个环境唯一的设置,例如数据库连接字符串(每个环境都有一个单独的数据库),我通常所做的是配置 Tomcat 服务器的 context.xml 文件,通过我的 Spring 应用程序中的 jndi 查找读取它并注入那些设置到我的 DAO 类中。我现在面临的挑战是弄清楚如何对需要注入到 spring-security-context.xml 文件中的 Active Directory 设置执行类似的操作。
截至目前,我已经在我的 spring-security-context.xml 文件中硬编码了我的 Active Directory 域和 url,但我不想这样,因为有一个不同的 Active Directory 系统我的每个环境。我想这让我感到困惑的是,我将构造函数和属性值从 spring-security-context.xml 文件注入到我的 ActiveDirectoryLdapAuthenticationProvider 类中,但是如何将这些设置注入到 spring-security-context.xml 文件中我的 Tomcat 服务器的 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 Server)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>
谁能帮助我了解如何将 Active Directory 域和 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:创建名为“org.springframework.security.filterChains”的bean时出错:无法解析对bean“org.springframework.security.web.DefaultSecurityFilterChain#3”的引用
谢谢!
-斯蒂芬斯
【问题讨论】:
【参考方案1】:我已经解决了我的问题,尽管该解决方案让我走上了与我最初尝试实施的不同的道路。我的最终目标是不对 Active Directory 服务器进行硬编码 我的 spring-security-context.xml 文件中的域和 URL,但要从我的 Tomcat 服务器上的外部源读取并注入这些值。我希望能够创建一个战争文件 为我的 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 文件,然后在以下文件夹中创建了 '\shared\lib' 文件夹(试图遵守约定):
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>
我停止并重新启动了我的本地 Tomcat 服务器,它成功了!我将 ExternalActiveDirectory.properties 文件的内容切换到不同的 AD 服务器,停止/重新启动 tomcat,然后再次尝试,只是为了确保它确实在工作,并且继续工作。
当我在 Linux 服务器上实现我的更改时,我对 catalina.properties 文件中的 shared.loader 行进行了相同的更改,但它的行号与我的 Tomcat 本地副本上的不同。另外,由于 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?
我希望这对其他人有所帮助!
-斯蒂芬斯
【讨论】:
【参考方案2】:它只是一个普通的可注入资源,所以像其他任何东西一样注入,一种方式:
Resource(name="ldapActiveDirectoryAuthProvider")
ActiveDirectoryLdapAuthenticationProvider myProvider;
或者你可以自动接线。无论哪种方式,您都可以正常访问其属性和字段。
【讨论】:
您好!我有点收集到它会是这样的,但我希望有一些关于我应该如何配置它的更具体的信息。我对 Spring 还是有点陌生。 我不能说得更具体,你有你需要的一切,有什么问题? 那是一个非常不同的问题,只是意味着您需要正确定义安全上下文,特别是没有定义 defaultsecruitychain bean。查看示例 security-context.xml 并确保您拥有一切...... 当我在 spring-security-context.xml 文件中进行硬编码时,Spring 安全性对我来说工作得很好。同样,我试图在我的 tomcat 服务器的 context.xml 文件中配置 Active Directory 域和 url,然后将这些值注入到我的 spring-security-context.xml 中,这样我就不必对它们进行硬编码。我相信我看到的错误与未正确注入的值有关。以上是关于如何从 context.xml 注入值的主要内容,如果未能解决你的问题,请参考以下文章
springmvc配置中,mapper一直依赖注入不进去的问题记录