- Published on
Spring Security Redirect Based on User Roles
- Authors
- Name
- Michael Whyte
- @mwhyte_dev
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 lesson by adding different user roles and showing and hiding front-end content based on these roles (User Roles and Thymeleaf Extras).
Today we'll be looking at redirecting users with different roles to different pages after they log in.
Admin.html
Following on from our previous example, we've now created a new HTML file called admin.html
. We will redirect our admin users to this new page when they log in.
<!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
Now, 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 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 in charge of overriding and configuring HttpSecurity explicitly. From the last example, we've added two lines.
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.
At this point, 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 will need to 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. We can imitate a front-end client making requests to the server from this.@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 up, we will be covering 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