java - 如何从 context.xml 注入(inject)值

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

我正在通过 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 查找读取它并注入(inject)那些设置到我的 DAO 类中。我现在面临的挑战是弄清楚如何对需要注入(inject)到我的 spring-security-context.xml 文件中的 Active Directory 设置执行类似的操作。

截至目前,我已经在我的 spring-security-context.xml 文件中硬编码了我的 Active Directory 域和 url,但我不想让它保持这样,因为有一个不同的 Active Directory 系统我的每个环境。我想这让我感到困惑的是,我正在从 spring-security-context.xml 文件中将构造函数和属性值注入(inject)到我的 ActiveDirectoryLdapAuthenticationProvider 类中,但是如何将这些设置注入(inject)到 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 服务器)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 文件中的 Active Directory 域和 url 设置注入(inject) spring-security-context.xml 文件?

编辑

domain 和 url 是我需要从 context.xml 中注入(inject)的两个值。但是,如果我要注入(inject)值 从 context.xml 文件中,我想我需要包含 ldapActiveDirectoryAuthProvider 所需的所有值 类(class)。我在想我需要添加的 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”的引用

谢谢!

-斯蒂芬斯

最佳答案

我已经解决了我的问题,尽管这个解决方案让我走上了一条与我最初尝试实现的路线不同的路线。我的最终目标是不对 Active Directory 服务器进行硬编码 我的 spring-security-context.xml 文件中的域和 URL,但要在我的 Tomcat 服务器上读入并注入(inject)来自外部源的值。我希望能够创建一个 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 文件,然后我在以下文件夹中创建了“\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?

我希望这对其他人有帮助!

-斯蒂芬斯

关于java - 如何从 context.xml 注入(inject)值,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/23809650/

相关文章:

java - 如何从列表 <Map<String, Object>> 中获取特定项目

java - 禁用 Spring mvc 自动序列化

Java Spring序列化json查看

java - 如何使用主键查找数据库中的行号(Android SQLite Eclipse Java)

java - Java中C++的Futures相当于什么

java - 使用 TLS 连接到 MariaDB 的 Tomcat 抛出 javax.naming.NamingException;命令行连接正常工作

tomcat - 设置在 Grails 中嵌入 Tomcat 以遵循符号链接(symbolic link)

java - 在 angular2 中发送文件

java - Nimbus 外观改变了 JButtons 的颜色

java - 在 tomcat 中重新部署新版本的 .war 时更改属性值