Skip to content
Advertisement

Spring Boot with container security

I’ve been using spring boot for some projects lately and I really like it. For a new project, we’d like to use tomcat-users.xml for really basic authentication, but I can’t figure out how to use the mechanism without a web.xml file. Most people using spring boot seem to be using spring security.

Is it possible to use tomcat container security with the spring boot java config model? I understand this breaks the runnable jar paradigm but we’re planning to deploy this as a war anyway.

Advertisement

Answer

As the Spring Security Project, I’d recommend using Spring Security as it does more than container Security (i.e. protection against common exploits, method security to provide defense in depth, etc). However, if you want to use container security you can as shown below:

You can use the following to use container managed security a Spring Boot 2.7.x or 3.0.0 Application:

Create a file at src/main/resources/tomcat-users.xml:

<?xml version='1.0' encoding='utf-8'?>
<tomcat-users>
    <role rolename="ROLE_USER"/>
    <user username="user" password="password" roles="ROLE_USER"/>
</tomcat-users>

Create the following Spring Configuration:

package example;

import java.net.URL;

import org.apache.catalina.Context;
import org.apache.catalina.authenticator.BasicAuthenticator;
import org.apache.catalina.realm.MemoryRealm;
import org.apache.catalina.realm.RealmBase;
import org.apache.tomcat.util.descriptor.web.LoginConfig;
import org.apache.tomcat.util.descriptor.web.SecurityCollection;
import org.apache.tomcat.util.descriptor.web.SecurityConstraint;

import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * @author Rob Winch
 */
@Configuration
public class ContainerSecurityConfiguration {

    @Bean
    public TomcatServletWebServerFactory servletContainer() {
        return new TomcatServletWebServerFactory() {
            @Override
            protected void postProcessContext(Context context) {
                context.getPipeline().addValve(new BasicAuthenticator());
                context.setRealm(createRealm());
                context.setLoginConfig(createLoginConfig());
                context.addSecurityRole("ROLE_USER");
                context.addConstraint(createSecurityConstraint());
            }

            private static RealmBase createRealm() {
                MemoryRealm memoryRealm = new MemoryRealm();
                memoryRealm.setPathname(getTomcatUsersPath());
                return memoryRealm;
            }

            // this must find the path to tomcat-users.xml, here we find 
            // it as a resource, but you can provide an absolute path to 
            // any place on your file system
            private static String getTomcatUsersPath() {
                String resourceName = "/tomcat-users.xml";
                URL resource = ContainerSecurityConfiguration.class.getResource(resourceName);
                if (resource == null) {
                    throw new IllegalStateException("Cannot find resource " + resourceName);
                }
                return resource.getPath();
            }

            private static LoginConfig createLoginConfig() {
                LoginConfig config = new LoginConfig();
                config.setRealmName("basic");
                config.setAuthMethod("BASIC");
                return config;
            }

            // /private/* -> ROLE_USER
            private static SecurityConstraint createSecurityConstraint() {
                SecurityConstraint securityConstraint = new SecurityConstraint();
                securityConstraint.setDisplayName("Private resources");
                SecurityCollection securityCollection = new SecurityCollection();
                securityCollection.addPattern("/private/*");
                securityConstraint.addCollection(securityCollection);
                securityConstraint.addAuthRole("ROLE_USER");
                return securityConstraint;
            }
        };
    }

}

Here is an automated test you can use:

package example;

import org.junit.jupiter.api.Test;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;

import static org.assertj.core.api.Assertions.assertThat;

@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
public class ApplicationTests {
    @Autowired
    TestRestTemplate rest;

    @Test
    public void authenticationRequired() {
        ResponseEntity<String> result = this.rest.getForEntity("/private", String.class);
        assertThat(result.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED);
    }

    @Test
    public void authorizationSuccess() {
        ResponseEntity<String> result = this.rest.withBasicAuth("user", "password").getForEntity("/private", String.class);
        assertThat(result.getStatusCode()).isEqualTo(HttpStatus.OK);
        assertThat(result.getBody()).isEqualTo("private message");
    }

}

No XML

If you do not want to use the xml file you can also manage your users pragmatically. First create a Tomcat Realm with the following:

package example;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.catalina.realm.GenericPrincipal;
import org.apache.catalina.realm.RealmBase;
/**
 * @author Rob Winch
 */
class ProgramaticMemoryRealm extends RealmBase {
    private final Map<String, UserInformation> usernameToPrincipal = new HashMap<>();

    public ProgramaticMemoryRealm(UserInformation... users) {
        this(List.of(users));
    }

    public ProgramaticMemoryRealm(List<UserInformation> users) {
        users.stream().forEach(this::addUser);
    }

    private void addUser(UserInformation user) {
        this.usernameToPrincipal.put(user.user().getName(), user);
    }

    @Override
    protected String getPassword(String username) {
        UserInformation userInformation = this.usernameToPrincipal.get(username);
        return userInformation == null ? null : userInformation.password();
    }

    @Override
    protected GenericPrincipal getPrincipal(String username) {
        UserInformation userInformation = this.usernameToPrincipal.get(username);
        return userInformation == null ? null : userInformation.user();
    }

    public record UserInformation(GenericPrincipal user, String password) {

    }
}

Now in ContainerSecurityConfiguration instead of using MemoryRealm use ProgramaticMemoryRealm:

private static RealmBase createRealm() {
    GenericPrincipal user = new GenericPrincipal("user", "password", List.of("ROLE_USER"));
    return new ProgramaticMemoryRealm(new UserInformation(user, "password"));
}

NOTE: We use UserInformation because in Spring Boot 3+ the GenericPrincipal password is not accessible and the constructor with the password is deprecated. In Spring Boot 2.7 UserInformation is not really necessary since the password is fully available on GenericPrincipal.

User contributions licensed under: CC BY-SA
6 People found this is helpful
Advertisement