So far, we've built a basic Spring Boot application, enabled Spring Security, and created a basic login form.
In the last lesson, we expanded on the first by adding different user roles and showing and hiding front-end content based on these roles (User Roles and Thymeleaf Extras).
Today, we'll look at redirecting users with different roles to different pages after logging in.
Admin.html
Following our previous example, we've created a new HTML file called admin.html. When our admin users log in, we will redirect them to this new page.
<!DOCTYPE html>
<html xmlns="<http://www.w3.org/1999/xhtml>" xmlns:th="<http://www.thymeleaf.org>">
<head>
<title>codenerve.com - Welcome!</title>
<meta charset="UTF-8">
<title>Admin</title>
<link href="<https://fonts.googleapis.com/css?family=Open+Sans:400,700>" rel="stylesheet">
<link rel="stylesheet" href="css/style.css">
</head>
<body>
<h1 th:inline="text">Hello [[${#httpServletRequest.remoteUser}]]!</h1>
<div>
Custom administrator page.
</div>
<br/>
<form th:action="@{/logout}" method="post">
<input type="submit" value="Sign Out"/>
</form>
</body>
</html>
MvcConfig
To serve the new admin.html page, we must add this page to our MvcConfig class.
As with the previous examples, this is accomplished by creating a class extending WebMvcConfigurerAdapter and overriding the addViewControllers method.
This time, we are adding all the earlier pages of our app and the new admin page:
package dev.mwhyte.spring.sec.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class MvcConfig implements WebMvcConfigurer {
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/").setViewName("login");
registry.addViewController("/index").setViewName("index");
registry.addViewController("/admin").setViewName("admin");
registry.addViewController("/login").setViewName("login");
}
}
WebSecurityConfig
The Constructor
To decide what to do when different user roles log in. We've created a new field of type AuthenticationSuccessHandler. We're setting this new configuration bean via constructor injection.
configure method
This method is responsible for explicitly overriding and configuring HttpSecurity. We've added two lines from the last example.
First, we've added a new antMatcher under the authorizeRequests section, and we've told spring security only to allow a user with the “ADMIN” role access to all endpoints starting with “/admin”:
.antMatchers("/admin").hasRole("ADMIN")
Secondly, we've added our CustomAuthenticationSuccessHandler under the formLogin section to tell Spring security to ask this CustomAuthenticationSuccessHandler what to do when a successful login occurs:
.successHandler(authenticationSuccessHandler)
configureGlobal method
The configureGlobal method is our in-memory registry of users. We've added two users. One with the primary USER role and the other with the ADMIN role.
Full example:
package dev.mwhyte.spring.sec.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
private AuthenticationSuccessHandler authenticationSuccessHandler;
@Autowired
public WebSecurityConfig(AuthenticationSuccessHandler authenticationSuccessHandler) {
this.authenticationSuccessHandler = authenticationSuccessHandler;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers( "/css/**", "/images/**", "/favicon.ico").permitAll()
.antMatchers("/admin").hasRole("ADMIN")
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login")
.successHandler(authenticationSuccessHandler)
.permitAll()
.and()
.logout()
.permitAll()
.and().csrf().disable(); // we'll enable this in a later blog post
}
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth
.inMemoryAuthentication()
.withUser("user").password("{noop}pass").roles("USER")
.and()
.withUser("admin").password("{noop}pass").roles("ADMIN");
}
}
CustomAuthenticationSuccessHandler
As you can see from our sample code below, this class implements Springs AuthenticationSuccessHandler class and overrides the onAuthenticationSuccess method.
Once a user successfully logs in, the onAuthenticationSuccess method is called, and the user's role is checked. If the user's role is admin, we redirect to the /admin HTTP endpoint; otherwise, we redirect them to the /index endpoint.
Our MvcConfig takes over and serves the correct HTML page based on the viewController we created previously.
package dev.mwhyte.spring.sec.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Set;
@Configuration
public class CustomAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
Set<String> roles = AuthorityUtils.authorityListToSet(authentication.getAuthorities());
if (roles.contains("ROLE_ADMIN")) {
httpServletResponse.sendRedirect("/admin");
} else {
httpServletResponse.sendRedirect("/index");
}
}
}
Demo
Check out the source code, open the Application class, and right-click to run the demo.
To start the example, port 8080 must be available on your machine. If it is not, you can change this default in the application.properties file using:
server.port=8081
Set this to whatever value you wish.
Alternatively, you can watch this short video:
Unit Testing
As always, we've amended and added some additional tests to cover the new functionality:
package dev.mwhyte.spring.sec;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestBuilders.FormLoginRequestBuilder;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.web.servlet.MockMvc;
import static org.hamcrest.core.StringContains.containsString;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestBuilders.formLogin;
import static org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers.authenticated;
import static org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers.unauthenticated;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
@ExtendWith(SpringExtension.class)
@SpringBootTest
@AutoConfigureMockMvc
public class ApplicationTests {
@Autowired
private MockMvc mockMvc;
@Test
public void loginWithValidUserThenAuthenticated() throws Exception {
FormLoginRequestBuilder login = formLogin()
.user("user")
.password("pass");
mockMvc.perform(login)
.andExpect(authenticated().withUsername("user"));
}
@Test
public void loginWithInvalidUserThenUnauthenticated() throws Exception {
FormLoginRequestBuilder login = formLogin()
.user("invalid")
.password("invalidpassword");
mockMvc.perform(login)
.andExpect(unauthenticated());
}
@Test
public void accessUnsecuredResourceThenOk() throws Exception {
mockMvc.perform(get("/css/style.css"))
.andExpect(status().isOk());
}
@Test
public void accessSecuredResourceUnauthenticatedThenRedirectsToLogin() throws Exception {
mockMvc.perform(get("/hello"))
.andExpect(status().is3xxRedirection())
.andExpect(redirectedUrlPattern("**/login"));
}
@Test
@WithMockUser
public void accessSecuredResourceAuthenticatedThenOk() throws Exception {
mockMvc.perform(get("/index"))
.andExpect(status().isOk());
}
@Test
@WithMockUser(roles = "USER")
public void loginWithRoleUserThenExpectAdminPageForbidden() throws Exception {
mockMvc.perform(get("/admin"))
.andExpect(status().isForbidden());
}
@Test
@WithMockUser(roles = "ADMIN")
public void loginWithRoleAdminThenExpectAdminContent() throws Exception {
mockMvc.perform(get("/admin"))
.andExpect(status().isOk())
.andExpect(content().string(containsString("Custom administrator page.")));
}
@Test
public void loginWithRoleUserThenExpectIndexPageRedirect() throws Exception {
FormLoginRequestBuilder login = formLogin()
.user("user")
.password("pass");
mockMvc.perform(login)
.andExpect(authenticated().withUsername("user"))
.andExpect(redirectedUrl("/index"));
}
@Test
public void loginWithRoleAdminThenExpectAdminPageRedirect() throws Exception {
FormLoginRequestBuilder login = formLogin()
.user("admin")
.password("pass");
mockMvc.perform(login)
.andExpect(authenticated().withUsername("admin"))
.andExpect(redirectedUrl("/admin"));
}
}
Here is a quick recap of the Spring test support we're using:
@ExtendWith(SpringExtension.class) — Tells JUnit5 to run unit tests with Spring's testing support
@SpringBootTest — Run as spring boot app. i.e. load application.properties and spring beans
@AutoConfigureMockMvc creates a Test helper class called MockMvc. From this, we can imitate a front-end client making requests to the server.
@WithMockUser — Provides the ability to mock certain users. An authenticated user in our case.
FormLoginRequestBuilder — A utility class that allows us to create a form-based login request.
Next, we will cover Spring Security's Cross-Site Request Forgery (CSRF) protection.
Resources
GitHub - mwhyte-dev/spring-security at 3.redirect-based-on-role
Build a Basic Login Form With Spring Security, Thymeleaf, and Java