How to easily integrate Spring Security with existing Spring MVC application.

How to easily integrate Spring Security with existing Spring MVC application.

without using XML

About Spring Security

Spring security is the easiest way to enable security in your spring application. spring-security enables you to easily integrate with frameworks like Spring Web MVC (or Spring Boot), as well as with standards like OAuth2 or SAML it auto-generates login/logout pages and protects against common exploits like CSRF. spring-security is simply a bunch of servlet filters.

Spring Security Provides

  1. Authentication -- verifying the identity of the user with (username/password)
  2. Authorization -- verifies if a user has permission over a resource. (like admin pages)

Let's begin by creating a simple spring-MVC application and integrate it with spring-security

  1. first create a spring maven project with a dynamic web project facet.
  2. create a base package called demo in src/main/java. and internally config and controller packages. Lets create a controller in src/main/java/demo/controller Create class BaseController A spring controller is simply created using @Controller annotation
package demo.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.servlet.ModelAndView;

@Controller
public class BaseController {
    @GetMapping("/")
    public ModelAndView showHome() {
        ModelAndView mav = new ModelAndView("dashboard");
        return mav;
    }
}

For adding spring security first we need SecurityWebApplicationInitializer which can be simply created by extending AbstractSecurityWebApplicationInitializer. that's it nothing else to do for this

package demo.config;

import org.springframework.security.web.context.AbstractSecurityWebApplicationInitializer;

public class SecurityWebApplicationInitializer extends AbstractSecurityWebApplicationInitializer {

}

now let's create a Security Config. this helps configure authorization and authentication on resources.

package demo.config;

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.core.userdetails.User;
import org.springframework.security.core.userdetails.User.UserBuilder;

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {


     @Override
     protected void configure(AuthenticationManagerBuilder auth) throws Exception {
         UserBuilder users = User.withDefaultPasswordEncoder();

         auth.inMemoryAuthentication()
             .withUser(users.username("skiran252").password("nothing").roles("USER"))
             .withUser(users.username("saikiran").password("admin").roles("ADMIN", "USER"));

/* creates an in-memory database for debugging with given users */
/* creates users with role ADMIN and USER can also create users with role of anytype which you can restrict with our choice*/
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests().antMatchers("/register/**").permitAll().antMatchers("/admin/**").hasRole("ADMIN").antMatchers("/css/**").permitAll().anyRequest().authenticated().and().formLogin().loginProcessingUrl("/loginprocessing");
    }

}

Description: the configure method using HTTP allows to customize access to routes for specific authenticated users http.authorizeRequests()..anyRequest().authenticated(); restricts all the routes to be used by a user only after authentication since we need specific routes before authentication like home and sign-in pages we allow them using .antMatchers("/register/**","/").permitAll() and also we can restrict access to any specific /admin with access only to admin .formLogin().loginProcessingUrl("/loginprocessing") creates a default login page for you and for processing login loginprocessingurl is managed by spring security internally. just ensure you don't have a same route under controller

since we are not using XML let's create beans using java configuration.

package demo.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.core.env.Environment;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import org.springframework.web.servlet.ViewResolver;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.view.InternalResourceViewResolver;

@Configuration
@EnableWebMvc
@EnableTransactionManagement
@ComponentScan(basePackages = "demo")
@PropertySource("classpath:app.properties")
public class ApplicationConfig implements WebMvcConfigurer {

    @Autowired
    private Environment env;

    @Bean
    public ViewResolver viewResolver() {
        InternalResourceViewResolver viewResolver = new InternalResourceViewResolver();
        viewResolver.setPrefix("/WEB-INF/view/");
        viewResolver.setSuffix(".jsp");
        return viewResolver;
    }

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/css/**").addResourceLocations("/css/");
        registry.addResourceHandler("/resources/**").addResourceLocations("/resources/");
    } // helps resolve CSS and resource files where they need to be searched by the
        // server
}

above config configures servlet dispatcher servlet which configures where to look our for view templates to be rendered by Controller

now most of the work is done. let's just add a final one to the dispatcher which dispatches servlets. we need to mention our config classes.

package demo.config;

import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;

public class MvcDispatcherServletInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {

    @Override
    protected Class<?>[] getRootConfigClasses() {
        return null;
    }

    @Override
    protected Class<?>[] getServletConfigClasses() {
        return new Class[] { ApplicationConfig.class };
    }

    @Override
    protected String[] getServletMappings() {

        return new String[] { "/" };
    }

}

That's it now you have a default login page provided by spring security

Let's connect it to the database

to connect to the database we need a dataSource. which can be created by defining a bean in Application Config. let's use Hibernate for now.

let's define some properties in src/main/resources/app.properties

# 
#JDBC
#

jdbc.driver=com.mysql.cj.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/spring
jdbc.user=root
jdbc.password=root

#
#connection pool
#

connection.pool.initialPoolSize=5
connection.pool.minPoolSize=5
connection.pool.maxPoolSize=20
connection.pool.maxIdleTime=3000

#
# Hibernate properties
#
hibernate.dialect=org.hibernate.dialect.MariaDBDialect
hibernate.show_sql=true
hibernate.packagesToScan=demo
hibernate.hbm2ddl.auto=update/create

Add these bean definitions to ApplicationConfig.java

package demo.config;

import java.sql.SQLException;
import java.util.Properties;
import java.util.logging.Logger;

import javax.sql.DataSource;

import org.apache.commons.dbcp2.BasicDataSource;
import org.hibernate.SessionFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.core.env.Environment;
import org.springframework.orm.hibernate5.HibernateTransactionManager;
import org.springframework.orm.hibernate5.LocalSessionFactoryBean;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import org.springframework.web.servlet.ViewResolver;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.view.InternalResourceViewResolver;

@Configuration
@EnableWebMvc
@EnableTransactionManagement
@ComponentScan(basePackages = "demo")
@PropertySource("classpath:app.properties")
public class ApplicationConfig implements WebMvcConfigurer {

    @Autowired
    private Environment env;

    @Bean
    public DataSource securityDataSource() throws SQLException {
        BasicDataSource ds = new BasicDataSource();
        ds.setUrl(env.getProperty("jdbc.url"));
        ds.setDriverClassName(env.getProperty("jdbc.driver"));
        ds.setUsername(env.getProperty("jdbc.user"));
        ds.setPassword(env.getProperty("jdbc.password"));
        ds.setInitialSize(this.getIntProperty("connection.pool.initialPoolSize"));
        ds.setMinIdle(5);
        ds.setMaxIdle(this.getIntProperty("connection.pool.maxIdleTime"));
        ds.setMaxOpenPreparedStatements(100);
        return ds;
    }

    @Bean
    public ViewResolver viewResolver() {
        InternalResourceViewResolver viewResolver = new InternalResourceViewResolver();
        viewResolver.setPrefix("/WEB-INF/view/");
        viewResolver.setSuffix(".jsp");
        return viewResolver;
    }

    private Properties getHibernateProperties() {
        Properties props = new Properties();
        props.setProperty("hibernate.dialect", env.getProperty("hibernate.dialect"));
        props.setProperty("hibernate.show_sql", env.getProperty("hibernate.show_sql"));
        props.setProperty("hibernate.hbm2ddl.auto",env.getProperty("hibernate.hbm2ddl.auto"));
        return props;
    }

    @Bean
    public LocalSessionFactoryBean sessionFactory() throws SQLException {
        LocalSessionFactoryBean sessionFactory = new LocalSessionFactoryBean();
        sessionFactory.setDataSource(securityDataSource());
        //where should hibernate look for classes to create entities sessionFactory.setPackagesToScan(env.getProperty("hibernate.packagesToScan"));
        sessionFactory.setHibernateProperties(getHibernateProperties());
        return sessionFactory;
    }

    @Bean
    @Autowired
    public HibernateTransactionManager transactionManager(SessionFactory sessionFactory) {
        HibernateTransactionManager txManager = new HibernateTransactionManager();
        txManager.setSessionFactory(sessionFactory);
        return txManager;
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

//parse string values to int
    @SuppressWarnings("unused")
    private int getIntProperty(String propName) {
        String propVal = env.getProperty(propName);
        int intPropVal = Integer.parseInt(propVal);
        return intPropVal;
    }

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        // TODO Auto-generated method stub
        registry.addResourceHandler("/css/**").addResourceLocations("/css/");
        registry.addResourceHandler("/resources/**").addResourceLocations("/resources/");
    }
}

and change this in the SecurityConfig.java

    @Autowired
    private DataSource securityDatasource; //will automatically look for dataSource bean created and sets this object

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.jdbcAuthentication().dataSource(securityDatasource);
    }

now it's almost done. one final step we need to define tables in the database database must of two tables users and authorities

schema authotities defines roles a user has like "ROLE_USER" with prepended "ROLE__"

+-----------+--------------+------+-----+---------+-------+
| Field     | Type         | Null | Key | Default | Extra |
+-----------+--------------+------+-----+---------+-------+
| username  | varchar(255) | NO   | PRI | NULL    |       |
| authority | varchar(255) | YES  |     | NULL    |       |
+-----------+--------------+------+-----+---------+-------+

users table

+-----------+--------------+------+-----+---------+-------+
| Field     | Type         | Null | Key | Default | Extra |
+-----------+--------------+------+-----+---------+-------+
| username  | varchar(255) | NO   | PRI | NULL    |       |
| enabled   | int          | NO   |     | NULL    |       |
| password  | varchar(255) | YES  |     | NULL    |       |
+-----------+--------------+------+-----+---------+-------+

done. we can easily add a password encoder by defining a bean in Application Config and encode password when storing in database. spring security will automatically do the rest for you.