Introduction
This tutorial will demonstrate how persistence is handled using Spring Security. We will implement a login system with a remember-me feature that shows how Spring Security handles data persistence.
Program Setup/Prerequisites
- Spring Boot 2.x;
- Spring Security;
- JPA;
- Thymeleaf;
- Database (MySQL).
Database Creation
Our database implementation supports MySQL but you can use any database of your choice provided there is an in-depth understanding of the documentation for your chosen database. Spring JPA was adopted for the query logic. The entities covered by our application include:
- User Entity;
- Roles Entity which will serve as authorization;
- A pivot table binding users and roles together.
Application implementation
The implementation followed a well-defined separation of concern with Data Access Logic separated from the Presentation Layer.
Defining our Data Source
The connection to the database was saved in application.properties. This contained all needed Hibernate properties and controls. Further documentation on some of the properties used in our application.properties file is available online on the Spring Boot page.
jdbc.driverClassName=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/persistenceDB?createDatabaseIfNotExist=true
jdbc.username=your choice
jdbc.password=your choice
spring.jpa.database=mysql
spring.jpa.hibernate.ddl-auto=update
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL57Dialect
hibernate.show_sql=false
spring.main.allow-circular-references=true
server.servlet.register-default-servlet = true
Bean Component Configuration
To include the data source, we simply apply the @Configuration annotation and set the @PropertySource annotation value to our database property file.
package com.valerio.userpersistency.springconfig.configImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
import org.springframework.jdbc.datasource.DriverManagerDataSource;
import javax.sql.DataSource;
@Configuration
public class DataSourceConfig {
@Autowired
Environment env;
@Bean(value = "dataSource")
public DataSource getDataSource() {
final DriverManagerDataSource dataSource = new DriverManagerDataSource();
dataSource.setDriverClassName(env.getProperty("jdbc.driverClassName"));
dataSource.setUrl(env.getProperty("jdbc.url"));
dataSource.setUsername(env.getProperty("jdbc.username"));
dataSource.setPassword(env.getProperty("jdbc.password"));
return dataSource;
}
}
Configuring our Spring Security
Using the filter chain approach for URL, we can setup a flow that ensures validation is in place. Our approach in this tutorial is simple and easy to follow:
Using the WebSecurityConfigurerAdapter. Here, we extended the WebSecurityConfigurerAdapter so as to have access to the associated method. The User detail was autowired, providing a dependency injection access. Remember that our focus is to ensure persistence, hence, we created the rememberme configuration alongside the persistence token. The PersistentTokenRepository is called to work with our custom persistence token setup. This handshake ensures that tokens are dynamically generated. We further implemented a means to save generated tokens in our database when rememberMe button is checked by the user.
@Configuration
@EnableWebSecurity
@PropertySource("classpath:application.properties")
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
@Qualifier("userDetailsService")
private UserDetailsService userDetailsService;
@Autowired
@Qualifier("authenticationSuccessHandler")
private AuthenticationSuccessHandler authenticationSuccessHandler;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/persistence", "/persistence/accessDenied").permitAll()
.and().exceptionHandling().accessDeniedPage("/persistence/accessDenied")
.and().csrf().disable();
// two variants of AuthorizedUrl configuration was done here
http.authorizeRequests()
.antMatchers("/persistence/homepage/Dashboard/**").access("hasRole('ADMIN')");
http.authorizeRequests()
.antMatchers("/persistence/homepage/user/**", "/persistence/redirect").hasRole("USER"); // "ROLE_" adds automatically
//login configuration was done here
http.formLogin()
.loginPage("/persistence")
.loginProcessingUrl("/loginFormPostTo")
.usernameParameter("myLoginPageUsernameParameterName")
.passwordParameter("myLoginPagePasswordParameterName")
.successHandler(authenticationSuccessHandler);
//remember me configuration was done here
http.rememberMe()
.tokenRepository(persistentTokenRepository())
.rememberMeParameter("myRememberMeParameterName")
.rememberMeCookieName("my-remember-me")
.tokenValiditySeconds(86400);
//logout configuration was done here
http.logout()
.logoutUrl("/Logout")
.logoutSuccessUrl("/persistence");
http.sessionManagement().maximumSessions(1);
}
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(getPasswordEncoder());
}
@Bean(value = "passwordEncoder")
public PasswordEncoder getPasswordEncoder() {
return new BCryptPasswordEncoder(7);
}
@Autowired
@Qualifier("dataSource")
DataSource dataSource;
@Bean
public PersistentTokenRepository persistentTokenRepository() {
CustomJdbcTokenImpl tokenRepository = new CustomJdbcTokenImpl();
tokenRepository.setDataSource(dataSource);
return tokenRepository;
}
}
Application Success Authentication Handler
This interface is used to implement successful authentication redirection. It is called by using the onAuthenticationSuccess method. Redirection to the success or authorised page occurs when the user authentication is successful.
@Component("authenticationSuccessHandler")
public class SuccessAuthenticationHandler implements AuthenticationSuccessHandler {
private final Logger logger = LoggerFactory.getLogger(SuccessAuthenticationHandler.class);
private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
@Override
public void onAuthenticationSuccess(HttpServletRequest request,
HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
String targetUrl = null;
Supplier<Stream<? extends GrantedAuthority>> sup = () -> authentication.getAuthorities().stream();
if(sup.get().anyMatch(a->(((GrantedAuthority) a).getAuthority().equals("ROLE_ADMIN")))) {
targetUrl = "/persistence/homepage/Dashboard";
} else if(sup.get().anyMatch(a->(((GrantedAuthority) a).getAuthority().equals("ROLE_USER")))) {
targetUrl = "/persistence/homepage/user";
} else {
targetUrl = "/persistence";
}
redirectStrategy.sendRedirect(request, response, targetUrl);
final HttpSession session = request.getSession(false);
if (session != null) {
// inactive interval in minutes between client requests before the servlet container will invalidate this session
session.setMaxInactiveInterval(10 * 60);
session.removeAttribute(WebAttributes.AUTHENTICATION_EXCEPTION);
}
}
}
Other implementations
In this tutorial, we implemented several spring features such as userRepository to handle everything that concern user access. Role management was carried out using the roleRepository service. To handle our static files, WebMvcConfigurer interface implementation enable us to override addResourceHandlers method. This method provides means to access static file saved in the resources folder.
Conclusion
We have demonstrated how to handle persistence using Spring Security. Complete source code implementation for this project can be found on our GitHub page.
1 thought on “Handling persistency using Spring Security”