1. Overview
In this quick tutorial, we’re going to take a look at how to define multiple entry points in a Spring Security application.
This mainly entails defining multiple http blocks in an XML configuration file or multiple HttpSecurity instances by extending the WebSecurityConfigurerAdapter class multiple times.
2. Maven Dependencies
For development, we will need the following dependencies:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> <version>1.5.2.RELEASE</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> <version>1.5.2.RELEASE</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <version>1.5.2.RELEASE</version> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-test</artifactId> <version>4.2.2.RELEASE</version> </dependency>
The latest versions of spring-boot-starter-security, spring-boot-starter-thymeleaf, spring-boot-starter-test, spring-security-test can be downloaded from Maven Central.
3. Defining Multiple Entry Points
3.1. Java Configuration
Let’s define the main configuration class that will hold a user source:
@Configuration @EnableWebSecurity public class MultipleEntryPointsSecurityConfig { @Bean public UserDetailsService userDetailsService() throws Exception { InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager(); manager.createUser(User .withUsername("user") .password("userPass") .roles("USER").build()); manager.createUser(User .withUsername("admin") .password("adminPass") .roles("ADMIN").build()); return manager; } }
When using Java configuration, the way to define multiple security realms is to define multiple configuration classes extending the WebSecurityConfigurerAdapter – each having its own security configuration. These can be static classes inside the main @Configuration class.
The main motivation for having multiple entry points in one application is if there are different types of users that can access different portions of the application.
Let’s look at an example – and define a configuration with three entry points, each having different permissions and authentication modes: one for administrative users using HTTP Basic Authentication, one for regular users that use form authentication, and one for guest users that do not require authentication.
The entry point defined for administrative users secures URLs of the form /admin/** to only allow users with a role of ADMIN and requires HTTP Basic Authentication:
@Configuration @Order(1) public static class App1ConfigurationAdapter extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http.antMatcher("/admin/**") .authorizeRequests().anyRequest().hasRole("ADMIN") .and().httpBasic() .and().exceptionHandling().accessDeniedPage("/403"); } }
The @Order annotation on each static class indicates the order in which the configurations will be considered to find one that matches the requested URL. The order value for each class must be unique.
Next, let’s define the configuration for URLs of the form /user/** that can be accessed by regular users with a USER role using form authentication:
@Configuration @Order(2) public static class App2ConfigurationAdapter extends WebSecurityConfigurerAdapter { protected void configure(HttpSecurity http) throws Exception { http.antMatcher("/user/**") .authorizeRequests().anyRequest().hasRole("USER") .and() .formLogin().loginPage("/userLogin").loginProcessingUrl("/user/login") .failureUrl("/userLogin?error=loginError") .defaultSuccessUrl("/user/myUserPage") .and() .logout().logoutUrl("/user/logout") .logoutSuccessUrl("/multipleHttpLinks") .deleteCookies("JSESSIONID") .and() .exceptionHandling().accessDeniedPage("/403") .and() .csrf().disable(); } }
This configuration will also require defining the /userLogin MVC mapping and a page that contains a standard login form.
For the form authentication, it’s very important to remember that any URL necessary for the configuration, such as the login processing URL also needs to follow the /user/** format or be otherwise configured to be accessible.
Both of the above configurations will redirect to a /403 URL if a user without the appropriate role attempts to access a protected URL.
Finally, let’s define the third configuration for URLs of the form /guest/** that will allow all types of users, including unauthenticated ones:
@Configuration @Order(3) public static class App3ConfigurationAdapter extends WebSecurityConfigurerAdapter { protected void configure(HttpSecurity http) throws Exception { http.antMatcher("/guest/**").authorizeRequests().anyRequest().permitAll(); } }
3.2. XML Configuration
Let’s take a look at the equivalent XML configuration for the three HttpSecurity instances in the previous section.
This will contain three separate XML <http> blocks.
For the /admin/** URLs the XML configuration will be:
<security:http pattern="/admin/**" use-expressions="true" auto-config="true"> <security:intercept-url pattern="/**" access="hasRole('ROLE_ADMIN')"/> <security:http-basic/> <security:access-denied-handler error-page="/403"/> </security:http>
Of note here is that if using XML configuration, the roles have to be of the form ROLE_<ROLE_NAME>.
The configuration for the /user/** URLs is:
<security:http pattern="/user/**" use-expressions="true" auto-config="true"> <security:intercept-url pattern="/**" access="hasRole('ROLE_USER')"/> <security:form-login login-page="/userLogin" login-processing-url="/user/login" authentication-failure-url="/userLogin?error=loginError" default-target-url="/user/myUserPage"/> <security:csrf disabled="true"/> <security:access-denied-handler error-page="/403"/> <security:logout logout-url="/user/logout" delete-cookies="JSESSIONID" logout-success-url="/multipleHttpLinks"/> </security:http>
For the /guest/** URLs we will have the http element:
<security:http pattern="/**" use-expressions="true" auto-config="true"> <security:intercept-url pattern="/guest/**" access="permitAll()"/> </security:http>
Also important here is that at least one XML <http> block must match the /** pattern.
4. Accessing Protected URLs
4.1. MVC Configuration
Let’s create request mappings that match the URL patterns we have secured:
@Controller public class PagesController { @RequestMapping("/admin/myAdminPage") public String getAdminPage() { return "multipleHttpElems/myAdminPage"; } @RequestMapping("/user/myUserPage") public String getUserPage() { return "multipleHttpElems/myUserPage"; } @RequestMapping("/guest/myGuestPage") public String getGuestPage() { return "multipleHttpElems/myGuestPage"; } @RequestMapping("/403") public String getAccessDeniedPage() { return "403"; } @RequestMapping("/multipleHttpLinks") public String getMultipleHttpLinksPage() { return "multipleHttpElems/multipleHttpLinks"; } }
The /multipleHttpLinks mapping will return a simple HTML page with links to the protected URLs:
<a th:href="@{/admin/myAdminPage}">Admin page</a> <a th:href="@{/user/myUserPage}">User page</a> <a th:href="@{/guest/myGuestPage}">Guest page</a>
Each of the HTML pages corresponding to the protected URLs will have a simple text and a back link:
Welcome admin! <a th:href="@{/multipleHttpLinks}" >Back to links</a>
4.2. Initializing the Application
We will run our example as a Spring Boot application, so let’s define a class with the main method:
@SpringBootApplication public class MultipleEntryPointsApplication { public static void main(String[] args) { SpringApplication.run(MultipleEntryPointsApplication.class, args); } }
If we want to use the XML configuration, we also need to add the @ImportResource({“classpath*:spring-security-multiple-entry.xml”}) annotation to our main class.
4.3. Testing the Security Configuration
Let’s set up a JUnit test class that we can use to test our protected URLs:
@RunWith(SpringRunner.class) @WebAppConfiguration @SpringBootTest(classes = MultipleEntryPointsApplication.class) public class MultipleEntryPointsTest { @Autowired private WebApplicationContext wac; @Autowired private FilterChainProxy springSecurityFilterChain; private MockMvc mockMvc; @Before public void setup() { this.mockMvc = MockMvcBuilders.webAppContextSetup(this.wac) .addFilter(springSecurityFilterChain).build(); } }
Next, let’s test the URLs using the admin user.
When requesting the /admin/adminPage URL without an HTTP Basic Authentication, we should expect to receive an Unauthorized status code, and after adding the authentication the status code should be 200 OK.
If attempting to access the /user/userPage URL with the admin user, we should receive status 302 Forbidden:
@Test public void whenTestAdminCredentials_thenOk() throws Exception { mockMvc.perform(get("/admin/myAdminPage")).andExpect(status().isUnauthorized()); mockMvc.perform(get("/admin/myAdminPage") .with(httpBasic("admin", "adminPass"))).andExpect(status().isOk()); mockMvc.perform(get("/user/myUserPage") .with(user("admin").password("adminPass").roles("ADMIN"))) .andExpect(status().isForbidden()); }
Let’s create a similar test using the regular user credentials to access the URLs:
@Test public void whenTestUserCredentials_thenOk() throws Exception { mockMvc.perform(get("/user/myUserPage")).andExpect(status().isFound()); mockMvc.perform(get("/user/myUserPage") .with(user("user").password("userPass").roles("USER"))) .andExpect(status().isOk()); mockMvc.perform(get("/admin/myAdminPage") .with(user("user").password("userPass").roles("USER"))) .andExpect(status().isForbidden()); }
In the second test, we can see that missing the form authentication will result in a status of 302 Found instead of Unauthorized, as Spring Security will redirect to the login form.
Finally, let’s create a test in which we access the /guest/guestPage URL will all three types of authentication and verify we receive a status of 200 OK:
@Test public void givenAnyUser_whenGetGuestPage_thenOk() throws Exception { mockMvc.perform(get("/guest/myGuestPage")).andExpect(status().isOk()); mockMvc.perform(get("/guest/myGuestPage") .with(user("user").password("userPass").roles("USER"))) .andExpect(status().isOk()); mockMvc.perform(get("/guest/myGuestPage") .with(httpBasic("admin", "adminPass"))) .andExpect(status().isOk()); }
5. Conclusion
In this tutorial, we have demonstrated how to configure multiple entry points when using Spring Security.
The complete source code for the examples can be found over on GitHub. To run the application, uncomment the MultipleEntryPointsApplication start-class tag in the pom.xml and run the command mvn spring-boot:run, then accesses the /multipleHttpLinks URL.
Note that it is not possible to log out when using HTTP Basic Authentication, so you will have to close and reopen the browser to remove this authentication.
To run the JUnit test, use the defined Maven profile entryPoints with the following command:
mvn clean install -PentryPoints