如何从context.xml注入值

时间:2014-05-22 14:25:55

标签: java tomcat spring-mvc spring-security context.xml

我正在通过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

2 个答案:

答案 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;

或者你可以自动装配。无论哪种方式,您都可以正常访问其属性和字段。