Spring Security extensions for Active Directory

Sat Oct 11 23:45:49 CST 2008发表于JRoller

We could not find information on how to integrate Spring Security with MS ActiveDirectory, so we developed LDAP provider for Active Directory Server, using existing LDAP provider as a base for the customization. 

The LDAP provider for Active Directory Server generates LDAP parameters with Active Directory - specific syntax. It consists of several classes.

ActiveDirectoryBindAuthenticator is an authenticator which binds as a user. Generates ActiveDirectory - specific syntax of LDAP parameters. Can only use DefaultSpringSecurityContextSource as contextSource. Similar to BindAuthenticator.

public class ActiveDirectoryBindAuthenticator implements LdapAuthenticator, MessageSourceAware {
    private final ContextSource contextSource;

    protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();

    protected final Log logger = LogFactory.getLog(this.getClass());

    public ActiveDirectoryBindAuthenticator(DefaultSpringSecurityContextSource contextSource) {
        Assert.notNull(contextSource, "contextSource must not be null.");
        this.contextSource = contextSource;
    }

    public DirContextOperations authenticate(Authentication authentication) {
        Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
            "Can only process UsernamePasswordAuthenticationToken objects");

        String username = authentication.getName();
        String password = (String) authentication.getCredentials();

        // Active Directory requires principalDn in the form: username-AT-dc1-DOT-dc2.
        String principalDn = determinePrincipalDn(username);

        DirContextOperations user = bindWithDn(principalDn, username, password);

        if (user == null) {
            throw new BadCredentialsException(messages.getMessage(
                "BindAuthenticator.badCredentials", "Bad credentials"));
        }

        // Store password in user: will be used by the authorities populator to bind (again)
        // as username/password.
        user.addAttributeValue(Context.SECURITY_CREDENTIALS, password);

        return user;
    }

    /**
     * Generates principalDn in the form:     username-AT-dc1-DOT-dc2
     * 
     * -AT-param username for which to generate the principalDn
     * -AT-return generated principalDn
     */
    protected String determinePrincipalDn(String username) {
        String rootDn = ((DefaultSpringSecurityContextSource) getContextSource())
            .getBaseLdapPathAsString();

        String principalDn = NamingUtils.preparePrincipalDn(username, rootDn);

        return principalDn;
    }

    protected static final String USER_SEARCH_FILTER = "(&(objectClass=user)(samAccountName={0}))";

    protected DirContextOperations bindWithDn(String principalDn, String username, String password) {
        if (this.logger.isDebugEnabled()) {
            this.logger.debug("Bind with dn=[" + principalDn + "], username=[" + username
                    + "]' password=[" + password + "]");
        }

        // bind as principalDn/password)
        SpringSecurityLdapTemplate template = new SpringSecurityLdapTemplate(
            new BindWithSpecificDnContextSource((SpringSecurityContextSource) getContextSource(),
                principalDn, password));

        // search for account info for username
        Object[] params = null;
        String base = "";

        String formattedFilter = MessageFormat
            .format(USER_SEARCH_FILTER, new Object[] { username });

        return template.searchForSingleEntry(base, formattedFilter, params);
    }

    public void setMessageSource(MessageSource messageSource) {
        Assert.notNull("Message source must not be null");
        this.messages = new MessageSourceAccessor(messageSource);
    }

    public ContextSource getContextSource() {
        return this.contextSource;
    }
}

ActiveDirectoryAuthoritiesPopulator obtains user role information from the directory, uses ActiveDirectory - specific syntax for LDAP parameters. It obtains roles by performing a search for "groups" the user is a member of. Can only use DefaultSpringSecurityContextSource as contextSource. Similar to DefaultLdapAuthoritiesPopulator.
  
public class ActiveDirectoryAuthoritiesPopulator implements LdapAuthoritiesPopulator {

    protected final Log logger = LogFactory.getLog(this.getClass());

    /**
     * A default role which will be assigned to all authenticated users if set
     */
    private GrantedAuthority defaultRole;

    private ContextSource contextSource;

    /**
     * Controls used to determine whether group searches should be performed over the full sub-tree
     * from the base DN. Modified by searchSubTree property
     */
    private final SearchControls searchControls = new SearchControls();

    /**
     * The ID of the attribute which contains the role name for a group
     */
    private String groupRoleAttribute = "cn";

    /**
     * The base DN from which the search for group membership should be performed
     */
    private String groupSearchBase;

    /**
     * The pattern to be used for the user search. {0} is the user's DN
     */
    private String groupSearchFilter = "member={0}";

    /**
     * Attributes of the User's LDAP Object that contain role name information.
     */
    private String rolePrefix = "ROLE_";

    private boolean convertToUpperCase = true;

    public ActiveDirectoryAuthoritiesPopulator(DefaultSpringSecurityContextSource contextSource,
            String groupSearchBase) {
        this.setContextSource(contextSource);
        this.setGroupSearchBase(groupSearchBase);
    }

    protected Set getAdditionalRoles(DirContextOperations user, String username) {
        return null;
    }

    public GrantedAuthority getDefaultRole() {
        return this.defaultRole;
    }

    public void setDefaultRole(GrantedAuthority defaultRole) {
        this.defaultRole = defaultRole;
    }

    private void setContextSource(DefaultSpringSecurityContextSource contextSource) {
        Assert.notNull(contextSource, "contextSource must not be null");
        this.contextSource = contextSource;
    }

    protected ContextSource getContextSource() {
        return contextSource;
    }

    private void setGroupSearchBase(String groupSearchBase) {
        Assert.notNull(groupSearchBase,
            "The groupSearchBase (name to search under), must not be null.");
        this.groupSearchBase = groupSearchBase;
        if (groupSearchBase.length() == 0) {
            logger
                .info("groupSearchBase is empty. Searches will be performed from the context source base");
        }
    }

    public String getGroupRoleAttribute() {
        return this.groupRoleAttribute;
    }

    public String getGroupSearchFilter() {
        return this.groupSearchFilter;
    }

    public String getRolePrefix() {
        return this.rolePrefix;
    }

    public boolean isConvertToUpperCase() {
        return this.convertToUpperCase;
    }

    public Set getGroupMembershipRoles(String userDn, String username, String password) {
        Set authorities = new HashSet();

        if (getGroupSearchBase() == null) {
            return authorities;
        }

        if (logger.isDebugEnabled()) {
            logger.debug("Searching for roles for user with DN = " + "'" + userDn
                    + "', with filter " + groupSearchFilter + " in search base '" + groupSearchBase
                    + "'");
        }

        String principalDn = determinePrincipalDn(username);

        // bind as principalDn/password
        ActiveDirectoryLdapTemplate template = new ActiveDirectoryLdapTemplate(
            new BindWithSpecificDnContextSource((SpringSecurityContextSource) getContextSource(),
                principalDn, password));
        template.setSearchControls(searchControls);

        // search for roles userDn is member of
        Set userRoles = template.searchForSingleAttributeValues(getGroupSearchBase(),
            groupSearchFilter, new String[] { userDn }, groupRoleAttribute);

        if (logger.isDebugEnabled()) {
            logger.debug("Roles from search: " + userRoles);
        }

        // convert role names to GrantedAuthorityImpl objects
        // for each role
        Iterator it = userRoles.iterator();
        while (it.hasNext()) {
            String role = (String) it.next();

            if (convertToUpperCase) {
                role = role.toUpperCase();
            }

            authorities.add(new GrantedAuthorityImpl(rolePrefix + role));
        }

        return authorities;
    }

    protected String getGroupSearchBase() {
        return groupSearchBase;
    }

    public void setConvertToUpperCase(boolean convertToUpperCase) {
        this.convertToUpperCase = convertToUpperCase;
    }

    public void setDefaultRole(String defaultRole) {
        Assert.hasLength(defaultRole, "defaultRole must be not empty");
        this.defaultRole = new GrantedAuthorityImpl(defaultRole);
    }

    public void setGroupRoleAttribute(String groupRoleAttribute) {
        Assert.hasLength(groupRoleAttribute, "groupRoleAttribute must be not empty");
        this.groupRoleAttribute = groupRoleAttribute;
    }

    public void setGroupSearchFilter(String groupSearchFilter) {
        Assert.hasLength(groupSearchFilter, "groupSearchFilter must be not empty");
        this.groupSearchFilter = groupSearchFilter;
    }

    public void setRolePrefix(String rolePrefix) {
        Assert.notNull(rolePrefix, "rolePrefix must not be null");
        this.rolePrefix = rolePrefix;
    }

    public void setSearchSubtree(boolean searchSubtree) {
        int searchScope = searchSubtree ? SearchControls.SUBTREE_SCOPE
                : SearchControls.ONELEVEL_SCOPE;
        searchControls.setSearchScope(searchScope);
    }

    public final GrantedAuthority[] getGrantedAuthorities(DirContextOperations user, String username) {
        String userDn = user.getNameInNamespace();

        if (logger.isDebugEnabled()) {
            logger.debug("Getting authorities for user " + userDn);
        }

        // password must be supplied by the ActiveDirectoryBindAuthenticator
        String password = user.getStringAttribute(Context.SECURITY_CREDENTIALS);

        Set roles = getGroupMembershipRoles(userDn, username, password);

        Set extraRoles = getAdditionalRoles(user, username);

        if (extraRoles != null) {
            roles.addAll(extraRoles);
        }

        if (defaultRole != null) {
            roles.add(defaultRole);
        }

        return (GrantedAuthority[]) roles.toArray(new GrantedAuthority[roles.size()]);
    }

    /**
     * Generates principalDn in the form:     username@dc1-DOT-dc2
     * 
     * @param username for which to generate the principalDn
     * @return generated principalDn
     */
    private String determinePrincipalDn(String username) {
        String rootDn = ((DefaultSpringSecurityContextSource) getContextSource())
            .getBaseLdapPathAsString();

        String principalDn = NamingUtils.preparePrincipalDn(username, rootDn);

        return principalDn;
    }
}

 

 

public class BindWithSpecificDnContextSource implements ContextSource {
    private final SpringSecurityContextSource ctxFactory;

    private final String userDn;

    private final String password;

    public BindWithSpecificDnContextSource(SpringSecurityContextSource ctxFactory, String userDn,
            String password) {
        this.ctxFactory = ctxFactory;
        this.userDn = userDn;
        this.password = password;
    }

    public DirContext getReadOnlyContext() throws DataAccessException {
        return ctxFactory.getReadWriteContext(userDn, password);
    }

    public DirContext getReadWriteContext() throws DataAccessException {
        return getReadOnlyContext();
    }
}

 

 

  
public final class NamingUtils {
    /**
     * This is a static class that should not be instantiated.
     */
    private NamingUtils() throws InstantiationException {
    }

    /**
     * Prepare principalDn in the form required by Active Directory:     username@dc1-DOT-dc2
     * 
     * @param username for which to generate the principalDn
     * @param rootDn For example: "DC=dc1,DC=dc2"
     * @return generated principalDn
     */
    public static String preparePrincipalDn(String username, String rootDn) {
        String principalDn = username + "@" + prepareDomainControllers(rootDn);

        return principalDn;
    }

    /**
     * Extracts DCs from ldap root parameter and prepares string: "dc1.dc2"
     * 
     * @param rootDn For example: "DC=dc1,DC=dc2"
     * @return The domain controllers string in Active Directory format.
     */
    public static String prepareDomainControllers(String rootDn) {
        List dcNameValues = StringUtils.tokenizeString(rootDn, ",");

        String domainControllers = "";
        for (int i = 0; i < dcNameValues.size(); i++) {
            if (i > 0) {
                domainControllers += ".";
            }

            List dcNameValue = StringUtils.tokenizeString((String) dcNameValues.get(i), "=");
            if (dcNameValue.size() == 2) {
                domainControllers += (String) dcNameValue.get(1);
            }
        }

        Assert.hasLength(domainControllers, "domainControllers must not be empty");

        return domainControllers;
    }
}

ActiveDirectoryLdapTemplate is an ActiveDirectory equivalent of the SpringSecurityLdapTemplate class. It simplifies ActiveDirectory access within Spring Security's ActiveDirectory-related services.

 

public class ActiveDirectoryLdapTemplate extends SpringSecurityLdapTemplate {
    protected final Log logger = LogFactory.getLog(this.getClass());

    private final SearchControls searchControls = new SearchControls();

    public ActiveDirectoryLdapTemplate(ContextSource contextSource) {
        super(contextSource);

        searchControls.setSearchScope(SearchControls.SUBTREE_SCOPE);
    }

    public Set searchForSingleAttributeValues(String base, String filter, Object[] filterArgs,
            final String attributeName) {
        final HashSet set = new HashSet();

        ContextMapper roleMapper = new ContextMapper() {
            public Object mapFromContext(Object ctx) {
                DirContextAdapter adapter = (DirContextAdapter) ctx;
                String[] values = adapter.getStringAttributes(attributeName);
                if (values == null || values.length == 0) {
                    logger.debug("No attribute value found for '" + attributeName + "'");
                } else {
                    set.addAll(Arrays.asList(values));
                }
                return null;
            }
        };

        SearchControls ctls = new SearchControls();
        ctls.setSearchScope(searchControls.getSearchScope());
        ctls.setReturningAttributes(new String[] { attributeName });
        // ActiveDirectory - specific
        ctls.setReturningObjFlag(true);

        // ActiveDirectory - specific
        search(base, filter, filterArgs, ctls, roleMapper);

        return set;
    }

    public List search(String base, String filter, Object[] filterArgs, SearchControls controls,
            ContextMapper mapper) {
        return search(base, filter, filterArgs, controls, mapper, new NullDirContextProcessor());
    }

    public List search(String base, String filter, Object[] filterArgs, SearchControls controls,
            ContextMapper mapper, DirContextProcessor processor) {
        ContextMapperCallbackHandler handler = new ContextMapperCallbackHandler(mapper);
        search(base, filter, filterArgs, controls, handler, processor);

        return handler.getList();
    }

    public void search(final String base, final String filter, final Object[] filterArgs,
            final SearchControls controls, NameClassPairCallbackHandler handler,
            DirContextProcessor processor) {

        // Create a SearchExecutor to perform the search.
        SearchExecutor se = new SearchExecutor() {
            public NamingEnumeration executeSearch(DirContext ctx)
                    throws javax.naming.NamingException {
                return ctx.search(base, filter, filterArgs, controls);
            }
        };

        search(se, handler, processor);
    }

    private final class NullDirContextProcessor implements DirContextProcessor {
        public void postProcess(DirContext ctx) throws NamingException {
            // Do nothing
        }

        public void preProcess(DirContext ctx) throws NamingException {
            // Do nothing
        }
    }
}

You can find source code (including unit and integration tests) in the Spring Security project JIRA: SEC-876 

阅读全文...
 
本站相关内容:(RSS)

MyFaces Security EL Extensions

MyFaces Security EL Extensions Posted by cagataycivici on November 20, 2006 Last week I’ve completed my work on the Security EL extension in Myfaces and added it to sandbox. By the help of SecurityContextVariableResolver and the SecurityContextPropertyResolver it’s possible to retrieve information from the underlying authentication/authorization mechanism that is used in the JSF application. The advantage of the resolver over existing enabledOnUserRole-visibleOnUserRole attributes is that th

List of Spring Extensions

Springframework.org - (28 reads)

Live

Live extension projects come with full source code, samples and documentation. Extensions are grouped according to the platform they target.

Incubator

Incubator extension projects are in the process of maturing until they are ready for becoming prime-time live extensions. Extensions are grouped according to the platform they target.

Want to propose a new Spring Extension?

If you think you have a project, either an existing project or maybe an entirely new idea, that would make a good Spring Extension then here's the process to follow.

Spring Extensions FAQ

Springframework.org - (30 reads)

What is Spring Extensions?

Spring Extensions is a new collaboration effort from SpringSource to encourage and enable community driven extensions to the Spring projects. It provides infrastructure, process and endorsement to selected projects that are consistent with the best of breed practices, principles and high quality associated with the SpringSource engineering teams.

How do I submit my project as a Spring Extension?

Please read these instructions to learn more about the proposal process.

What's in it for me?

Every Spring Extension is officially endorsed by SpringSource. While not every extension is developed or contributed by a SpringSource employee, every extension has a SpringSource sponsor who is responsible for working with the project lead to make the extension as successful as possible. As well as an official endorsement, SpringSource also provide a development environment and services, including:

  • dedicated subversion repository
  • personal licensed copy of the SpringSource Tool Suite for each contributor
  • distribution mechanism
  • continuous build server
  • release cycle and (very limited) project management co-ordination
  • bug and enhancement management (JIRA)
  • forums
  • mailing list

How do you differ from SourceForge, code.google.com etc.?

SourceForge provides development infrastructure and is very good at it. It also provides a public distribution channel. What it doesn't provide are project management capabilities (release management, etc.). Critically, it doesn't provide any endorsement or affiliation with the hosted code. Spring Extensions provides the infrastructure but also provides the endorsement and quality assurance that is expected from the strong association with SpringSource.

What happens to my extension once it has been created?

Every extension starts off in the incubator, at version 0.1. If appropriate, after reaching a level of maturity and stability, the extension will be released as "live", where it will continue to grow. If the extension becomes redundant (maybe the portfolio projects provide an alternative, or the requirements are no longer relevant) it will be archived. If the extension is of a particular interest to a product lead, it may be incorporated into that full Spring project, or in some cases it might be that an extension represents enough value to the community to become a Spring project in its own right.

Are the extensions purely Java based? What about extensions to Spring.NET? Python?

Spring extensions are not limited to Java. There are currently a number of extensions related to the Spring.NET project, and we even have a Python extension. The process and infrastructure (except for obvious differences like build servers and IDEs) are the same for both .NET, Java or any other type of extension.

How does this relate to Spring Modules?

Spring Modules was developed to serve a similar goal and was very successful. In fact, it was too successful and grew beyond anybody's expectations. As such, Spring Modules isn't really flexible enough or scaleable enough to manage a large number of independent projects. Spring Modules has now reached end of life. The modules that are still undergoing active development may be ported to Spring Extensions.

What restrictions are there for a Spring Extension?

Every extension must meet and follow the development standards of a Spring project. These include:

  • build system
  • various coding and design standards (recommended programming and design patterns etc.)
  • package layout (org.springextensions.<project>)
  • regular architecture, design and code reviews

An extension must also meet the following criteria:

  • actively being maintained and enhanced by the contributors

Additionally, every contributor to an extension must:

  • sign a legal contract assigning copyright to SpringSource
  • agree to follow the development standards

Why does every extension require a SpringSource sponsor? What do they do?

The sponsor has two primary responsibilities, co-ordinating that extension with a particular project and quality assurance.

In terms of co-ordination, the sponsor will be a good channel through which the contributors and the project lead communicate, feeding ideas back and forth. Of course, in many cases the sponsor will be the product lead. For quality assurance, the sponsor is accountable within SpringSource for ensuring the project is up to scratch and consistent with other teams and for this reason the sponsor has the final say on any contested decisions related to the extension.

Can I just give you my code?

Not really. Spring Extensions is about enabling community driven development. It isn't a way of "off-loading" code. When submitting a potential extension, you will be expected to define the roadmap and give an idea of your resource availability to develop the extension.

The key point is that while SpringSource may contribute to the code base, it is still your code base.

The other key point is that an extension needs to be more than just code. A good extension (like any other Spring project) should have API documentation, reference documentation, samples, examples, etc.

So can anything be a Spring Extension?

Again, no, not really. Each extension will have an internal sponsor assigned to it, and obviously only projects that are interesting and relevant to each sponsor will be accepted.

What about licensing?

Every Spring Extension, as part of the proposal process, is licensed under the Apache License, version 2.0. A copy of that license is available here.

What about copyright?

Similar to many open source projects, we request that contributors assign their copyright to us when contributing code to a product or project owned or managed by us and this includes Spring Extensions. The purpose of copyright assignment is to clearly define the terms related to intellectual property contributions to the extension and this allows us to properly and completely defend the extension should there be a legal dispute regarding the extension in the future.

What would make a good Spring Extension?

This is very hard to answer definitively. If it is an extension to the framework, or applying the Spring programming model to a new area or environment, is still actively developed and is of a significant size (more than one or two classes!), then it's time to think about submitting a proposal.

To get a feel for extension projects that are already in the incubator take a look at the existing list of Spring Extensions .

Spring Extensions Home

Springframework.org - (28 reads)

Introducing Spring Extensions!

We are pleased to announce Spring Extensions; a new venture by SpringSource to encourage and support quality community contributed extensions to the Spring projects and programming model. For more information check out the following links:

Want to propose a new Spring Extension?

If you think you have a project, either existing or just an idea, that would make a good Spring Extension then here's the process to follow.

互联网相关内容:
MyFaces Security EL Extensions (2007年11月24日)
Spring Security - Spring Security (2008年04月21日)
List of Spring Extensions (2008年10月14日)
Spring Extensions FAQ (2008年10月14日)
Spring Extensions Home (2008年10月14日)
List of Spring Extensions (2008年10月14日)
SOAP Security Extensions: Digital Signature (2007年11月07日)
Spring Security and Java Security Community (2008年04月17日)
Spring Security and Java Security Community (2008年04月17日)