如何从 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 注入值的主要内容,如果未能解决你的问题,请参考以下文章

Spring MVC 中的不同上下文是如何工作的?

springmvc配置中,mapper一直依赖注入不进去的问题记录

未从 context.xml 文件中的属性文件获取值

spring无法读取properties文件数据

如何从 AppSettings 读取配置值并将配置注入接口实例

如何从HttpSession而不是会话中注入一个值