In this post, we will build a full-blown Spring MVC
application secured using Spring Security
, integrating with MySQL
database using Hibernate
, handling Many-to-Many relationship on view, storing passwords in encrypted format using BCrypt
, and providing RememberMe
functionality using custom PersistentTokenRepository
implementation with Hibernate HibernateTokenRepositoryImpl
, retrieving the records from database and updating or deleting them within transaction
, all using annotation configuration. Let’s get going.
This project can be served as a template for your own Spring MVC projects integrating Spring Security.
Note:
This post demonstrates a complete application with complete code. In order to manage the size of the post, i have skipped the textual descriptions of some basic stuff. In case you are interested in those details, this ,this & this post will help you.
Summary:
The project shows a simple user-management application. One can create a new user, edit or delete an existing user, and list all the users. User can be associated with one or more UserProfile, showing many-to-many relationship. URL’s of the applications are secured using Spring Security. That means, based on the roles of logged in user, access to certain URL’s will be granted or prohibited. On the view layer, user will see only the content he/she is allowed to based on the roles assigned to him/her, thanks to Spring Security tags for view layer.
- Spring Boot+AngularJS+Spring Data+Hibernate+MySQL CRUD App
- Secure Spring REST API using OAuth2
- Spring Boot REST API Tutorial
- Spring Boot WAR deployment example
- Spring Boot Introduction + Hello World Example
- AngularJS+Spring Security using Basic Authentication
- Secure Spring REST API using Basic Authentication
- Spring 4 Email Template Library Example
- Spring 4 Caching Annotations Tutorial
- Spring 4 Cache Tutorial with EhCache
- Spring 4 MVC+JPA2+Hibernate Many-to-many Example
- Spring 4 Email With Attachment Tutorial
- Spring 4 Email Integration Tutorial
- Spring MVC 4+JMS+ActiveMQ Integration Example
- Spring 4+JMS+ActiveMQ @JmsLister @EnableJms Example
- Spring 4+JMS+ActiveMQ Integration Example
- Spring MVC 4+Apache Tiles 3 Integration Example
- Spring MVC 4+AngularJS Example
- Spring MVC 4+AngularJS Server communication example : CRUD application using ngResource $resource service
- Spring MVC 4+AngularJS Routing with UI-Router Example
- Spring MVC 4+Hibernate 4 Many-to-many JSP Example
- Spring MVC 4+Hibernate 4+MySQL+Maven integration + Testing example using annotations
- Spring Security 4 Hibernate Integration Annotation+XML Example
- Spring MVC4 FileUpload-Download Hibernate+MySQL Example
- Spring MVC 4 Form Validation and Resource Handling
- Spring Batch- MultiResourceItemReader & HibernateItemWriter example
Following technologies being used:
- Spring 4.2.5.RELEASE
- Spring Security 4.0.4.RELEASE
- Hibernate Core 4.3.11.Final
- validation-api 1.1.0.Final
- hibernate-validator 5.1.3.Final
- MySQL Server 5.6
- Maven 3
- JDK 1.7
- Tomcat 8.0.21
- Eclipse MARS.1 Release 4.5.1
- logback 1.1.7
Let’s begin.
Step 1: Create the directory structure
Following will be the final project structure:
Let’s now add the content mentioned in above structure explaining each in detail.
Step 2: Update pom.xml to include required dependencies
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <modelVersion>4.0.0</modelVersion> <groupId>com.websystique.springmvc</groupId> <artifactId>SpringMVCHibernateManyToManyCRUDExample</artifactId> <packaging>war</packaging> <version>1.0.0</version> <name>SpringMVCHibernateWithSpringSecurityExample</name> <properties> <springframework.version>4.2.5.RELEASE</springframework.version> <springsecurity.version>4.0.4.RELEASE</springsecurity.version> <hibernate.version>4.3.11.Final</hibernate.version> <mysql.connector.version>5.1.31</mysql.connector.version> </properties> <dependencies> <!-- Spring --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-core</artifactId> <version>${springframework.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-web</artifactId> <version>${springframework.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>${springframework.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-tx</artifactId> <version>${springframework.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-orm</artifactId> <version>${springframework.version}</version> </dependency> <!-- Spring Security --> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-web</artifactId> <version>${springsecurity.version}</version> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-config</artifactId> <version>${springsecurity.version}</version> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-taglibs</artifactId> <version>${springsecurity.version}</version> </dependency> <!-- Hibernate --> <dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-core</artifactId> <version>${hibernate.version}</version> </dependency> <!-- jsr303 validation --> <dependency> <groupId>javax.validation</groupId> <artifactId>validation-api</artifactId> <version>1.1.0.Final</version> </dependency> <dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-validator</artifactId> <version>5.1.3.Final</version> </dependency> <!-- MySQL --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>${mysql.connector.version}</version> </dependency> <!-- SLF4J/Logback --> <dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-classic</artifactId> <version>1.1.7</version> </dependency> <!-- Servlet+JSP+JSTL --> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>3.1.0</version> </dependency> <dependency> <groupId>javax.servlet.jsp</groupId> <artifactId>javax.servlet.jsp-api</artifactId> <version>2.3.1</version> </dependency> <dependency> <groupId>javax.servlet</groupId> <artifactId>jstl</artifactId> <version>1.2</version> </dependency> </dependencies> <build> <pluginManagement> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.2</version> <configuration> <source>1.7</source> <target>1.7</target> </configuration> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-war-plugin</artifactId> <version>2.4</version> <configuration> <warSourceDirectory>src/main/webapp</warSourceDirectory> <warName>SpringMVCHibernateWithSpringSecurityExample</warName> <failOnMissingWebXml>false</failOnMissingWebXml> </configuration> </plugin> </plugins> </pluginManagement> <finalName>SpringMVCHibernateWithSpringSecurityExample</finalName> </build> </project>
Step 3: Configure Security
The first and foremost step to add spring security in our application is to create Spring Security Java Configuration. This configuration creates a Servlet Filter known as the springSecurityFilterChain
which is responsible for all the security (protecting the application URLs, validating submitted username and passwords, redirecting to the log in form, etc) within our application
package com.websystique.springmvc.security; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.authentication.AuthenticationTrustResolver; import org.springframework.security.authentication.AuthenticationTrustResolverImpl; import org.springframework.security.authentication.dao.DaoAuthenticationProvider; 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.UserDetailsService; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.authentication.rememberme.PersistentTokenBasedRememberMeServices; import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository; @Configuration @EnableWebSecurity public class SecurityConfiguration extends WebSecurityConfigurerAdapter { @Autowired @Qualifier("customUserDetailsService") UserDetailsService userDetailsService; @Autowired PersistentTokenRepository tokenRepository; @Autowired public void configureGlobalSecurity(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService); auth.authenticationProvider(authenticationProvider()); } @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests().antMatchers("/", "/list") .access("hasRole('USER') or hasRole('ADMIN') or hasRole('DBA')") .antMatchers("/newuser/**", "/delete-user-*").access("hasRole('ADMIN')").antMatchers("/edit-user-*") .access("hasRole('ADMIN') or hasRole('DBA')").and().formLogin().loginPage("/login") .loginProcessingUrl("/login").usernameParameter("ssoId").passwordParameter("password").and() .rememberMe().rememberMeParameter("remember-me").tokenRepository(tokenRepository) .tokenValiditySeconds(86400).and().csrf().and().exceptionHandling().accessDeniedPage("/Access_Denied"); } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Bean public DaoAuthenticationProvider authenticationProvider() { DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider(); authenticationProvider.setUserDetailsService(userDetailsService); authenticationProvider.setPasswordEncoder(passwordEncoder()); return authenticationProvider; } @Bean public PersistentTokenBasedRememberMeServices getPersistentTokenBasedRememberMeServices() { PersistentTokenBasedRememberMeServices tokenBasedservice = new PersistentTokenBasedRememberMeServices( "remember-me", userDetailsService, tokenRepository); return tokenBasedservice; } @Bean public AuthenticationTrustResolver getAuthenticationTrustResolver() { return new AuthenticationTrustResolverImpl(); } }
As shown above, the access to URLs is governed as follows:
- ‘/’ & ‘/list’ : Accessible to everyone
- ‘/newuser’ & ‘/delete-user-*’ : Accessible only to Admin
- ‘/edit-user-*’ : Accessible to Admin & DBA
Since we are storing the credentials in database, configuring DaoAuthenticationProvider
with UserDetailsService
would come handy. Additionally, in order to encrypt the password in database, we have chosen BCryptPasswordEncoder
. Moreover, since we will also provide RememberMe functionality, keeping track of token-data in database, we configured a PersistentTokenRepository
implementation.
Spring Security comes with two implementation of PersistentTokenRepository : JdbcTokenRepositoryImpl and InMemoryTokenRepositoryImpl. We could have opted for JdbcTokenRepositoryImpl [this post demonstrates the RememberMe with JdbcTokenRepositoryImpl], but since we are using Hibernate in our application, why not create a custom implementation using Hibernate instead of using JDBC? Shown below is an attempt for the same.
package com.websystique.springmvc.dao; import java.util.Date; import org.hibernate.Criteria; import org.hibernate.criterion.Restrictions; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.security.web.authentication.rememberme.PersistentRememberMeToken; import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository; import org.springframework.stereotype.Repository; import org.springframework.transaction.annotation.Transactional; import com.websystique.springmvc.dao.AbstractDao; import com.websystique.springmvc.model.PersistentLogin; @Repository("tokenRepositoryDao") @Transactional public class HibernateTokenRepositoryImpl extends AbstractDao<String, PersistentLogin> implements PersistentTokenRepository { static final Logger logger = LoggerFactory.getLogger(HibernateTokenRepositoryImpl.class); @Override public void createNewToken(PersistentRememberMeToken token) { logger.info("Creating Token for user : {}", token.getUsername()); PersistentLogin persistentLogin = new PersistentLogin(); persistentLogin.setUsername(token.getUsername()); persistentLogin.setSeries(token.getSeries()); persistentLogin.setToken(token.getTokenValue()); persistentLogin.setLast_used(token.getDate()); persist(persistentLogin); } @Override public PersistentRememberMeToken getTokenForSeries(String seriesId) { logger.info("Fetch Token if any for seriesId : {}", seriesId); try { Criteria crit = createEntityCriteria(); crit.add(Restrictions.eq("series", seriesId)); PersistentLogin persistentLogin = (PersistentLogin) crit.uniqueResult(); return new PersistentRememberMeToken(persistentLogin.getUsername(), persistentLogin.getSeries(), persistentLogin.getToken(), persistentLogin.getLast_used()); } catch (Exception e) { logger.info("Token not found..."); return null; } } @Override public void removeUserTokens(String username) { logger.info("Removing Token if any for user : {}", username); Criteria crit = createEntityCriteria(); crit.add(Restrictions.eq("username", username)); PersistentLogin persistentLogin = (PersistentLogin) crit.uniqueResult(); if (persistentLogin != null) { logger.info("rememberMe was selected"); delete(persistentLogin); } } @Override public void updateToken(String seriesId, String tokenValue, Date lastUsed) { logger.info("Updating Token for seriesId : {}", seriesId); PersistentLogin persistentLogin = getByKey(seriesId); persistentLogin.setToken(tokenValue); persistentLogin.setLast_used(lastUsed); update(persistentLogin); } }
Above implementation uses an Entity [PersistentLogin] mapped to persistent_logins table, shown below is the entity itself.
package com.websystique.springmvc.model; import java.io.Serializable; import java.util.Date; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.Id; import javax.persistence.Table; import javax.persistence.Temporal; import javax.persistence.TemporalType; @Entity @Table(name="PERSISTENT_LOGINS") public class PersistentLogin implements Serializable{ @Id private String series; @Column(name="USERNAME", unique=true, nullable=false) private String username; @Column(name="TOKEN", unique=true, nullable=false) private String token; @Temporal(TemporalType.TIMESTAMP) private Date last_used; public String getSeries() { return series; } public void setSeries(String series) { this.series = series; } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getToken() { return token; } public void setToken(String token) { this.token = token; } public Date getLast_used() { return last_used; } public void setLast_used(Date last_used) { this.last_used = last_used; } }
The UserDetailsService implementation, used in Security configuration is shown below:
package com.websystique.springmvc.security; import java.util.ArrayList; import java.util.List; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import com.websystique.springmvc.model.User; import com.websystique.springmvc.model.UserProfile; import com.websystique.springmvc.service.UserService; @Service("customUserDetailsService") public class CustomUserDetailsService implements UserDetailsService{ static final Logger logger = LoggerFactory.getLogger(CustomUserDetailsService.class); @Autowired private UserService userService; @Transactional(readOnly=true) public UserDetails loadUserByUsername(String ssoId) throws UsernameNotFoundException { User user = userService.findBySSO(ssoId); logger.info("User : {}", user); if(user==null){ logger.info("User not found"); throw new UsernameNotFoundException("Username not found"); } return new org.springframework.security.core.userdetails.User(user.getSsoId(), user.getPassword(), true, true, true, true, getGrantedAuthorities(user)); } private List<GrantedAuthority> getGrantedAuthorities(User user){ List<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>(); for(UserProfile userProfile : user.getUserProfiles()){ logger.info("UserProfile : {}", userProfile); authorities.add(new SimpleGrantedAuthority("ROLE_"+userProfile.getType())); } logger.info("authorities : {}", authorities); return authorities; } }
Finally, register the springSecurityFilter with application war using below mentioned initializer class.
package com.websystique.springmvc.security; import org.springframework.security.web.context.AbstractSecurityWebApplicationInitializer; public class SecurityWebApplicationInitializer extends AbstractSecurityWebApplicationInitializer { }
That’s all with Spring Security Configuration. Now let’s begin with Spring MVC part, discussing Hibernate configuration, necessary DAO, models & services along the way.
Step 4: Configure Hibernate
package com.websystique.springmvc.configuration; import java.util.Properties; import javax.sql.DataSource; 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.jdbc.datasource.DriverManagerDataSource; import org.springframework.orm.hibernate4.HibernateTransactionManager; import org.springframework.orm.hibernate4.LocalSessionFactoryBean; import org.springframework.transaction.annotation.EnableTransactionManagement; @Configuration @EnableTransactionManagement @ComponentScan({ "com.websystique.springmvc.configuration" }) @PropertySource(value = { "classpath:application.properties" }) public class HibernateConfiguration { @Autowired private Environment environment; @Bean public LocalSessionFactoryBean sessionFactory() { LocalSessionFactoryBean sessionFactory = new LocalSessionFactoryBean(); sessionFactory.setDataSource(dataSource()); sessionFactory.setPackagesToScan(new String[] { "com.websystique.springmvc.model" }); sessionFactory.setHibernateProperties(hibernateProperties()); return sessionFactory; } @Bean public DataSource dataSource() { DriverManagerDataSource dataSource = new DriverManagerDataSource(); dataSource.setDriverClassName(environment.getRequiredProperty("jdbc.driverClassName")); dataSource.setUrl(environment.getRequiredProperty("jdbc.url")); dataSource.setUsername(environment.getRequiredProperty("jdbc.username")); dataSource.setPassword(environment.getRequiredProperty("jdbc.password")); return dataSource; } private Properties hibernateProperties() { Properties properties = new Properties(); properties.put("hibernate.dialect", environment.getRequiredProperty("hibernate.dialect")); properties.put("hibernate.show_sql", environment.getRequiredProperty("hibernate.show_sql")); properties.put("hibernate.format_sql", environment.getRequiredProperty("hibernate.format_sql")); return properties; } @Bean @Autowired public HibernateTransactionManager transactionManager(SessionFactory s) { HibernateTransactionManager txManager = new HibernateTransactionManager(); txManager.setSessionFactory(s); return txManager; } }
Below is the properties file used in this post.
/src/main/resources/application.properties
jdbc.driverClassName = com.mysql.jdbc.Driver jdbc.url = jdbc:mysql://localhost:3306/websystique jdbc.username = myuser jdbc.password = mypassword hibernate.dialect = org.hibernate.dialect.MySQLDialect hibernate.show_sql = true hibernate.format_sql = true
Step 5: Configure Spring MVC
package com.websystique.springmvc.configuration; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.MessageSource; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.context.support.ResourceBundleMessageSource; import org.springframework.format.FormatterRegistry; import org.springframework.web.servlet.config.annotation.EnableWebMvc; import org.springframework.web.servlet.config.annotation.PathMatchConfigurer; import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; import org.springframework.web.servlet.config.annotation.ViewResolverRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter; import org.springframework.web.servlet.view.InternalResourceViewResolver; import org.springframework.web.servlet.view.JstlView; import com.websystique.springmvc.converter.RoleToUserProfileConverter; @Configuration @EnableWebMvc @ComponentScan(basePackages = "com.websystique.springmvc") public class AppConfig extends WebMvcConfigurerAdapter{ @Autowired RoleToUserProfileConverter roleToUserProfileConverter; /** * Configure ViewResolvers to deliver preferred views. */ @Override public void configureViewResolvers(ViewResolverRegistry registry) { InternalResourceViewResolver viewResolver = new InternalResourceViewResolver(); viewResolver.setViewClass(JstlView.class); viewResolver.setPrefix("/WEB-INF/views/"); viewResolver.setSuffix(".jsp"); registry.viewResolver(viewResolver); } /** * Configure ResourceHandlers to serve static resources like CSS/ Javascript etc... */ @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/static/**").addResourceLocations("/static/"); } /** * Configure Converter to be used. * In our example, we need a converter to convert string values[Roles] to UserProfiles in newUser.jsp */ @Override public void addFormatters(FormatterRegistry registry) { registry.addConverter(roleToUserProfileConverter); } /** * Configure MessageSource to lookup any validation/error message in internationalized property files */ @Bean public MessageSource messageSource() { ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource(); messageSource.setBasename("messages"); return messageSource; } /**Optional. It's only required when handling '.' in @PathVariables which otherwise ignore everything after last '.' in @PathVaidables argument. * It's a known bug in Spring [https://jira.spring.io/browse/SPR-6164], still present in Spring 4.1.7. * This is a workaround for this issue. */ @Override public void configurePathMatch(PathMatchConfigurer matcher) { matcher.setUseRegisteredSuffixPatternMatch(true); } }
The main highlight of this configuration is RoleToUserProfileConverter. It will take care of mapping the individual userProfile id’s on view to actual UserProfile Entities in database.
package com.websystique.springmvc.converter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.convert.converter.Converter; import org.springframework.stereotype.Component; import com.websystique.springmvc.model.UserProfile; import com.websystique.springmvc.service.UserProfileService; /** * A converter class used in views to map id's to actual userProfile objects. */ @Component public class RoleToUserProfileConverter implements Converter<Object, UserProfile>{ static final Logger logger = LoggerFactory.getLogger(RoleToUserProfileConverter.class); @Autowired UserProfileService userProfileService; /** * Gets UserProfile by Id * @see org.springframework.core.convert.converter.Converter#convert(java.lang.Object) */ public UserProfile convert(Object element) { Integer id = Integer.parseInt((String)element); UserProfile profile= userProfileService.findById(id); logger.info("Profile : {}",profile); return profile; } }
Since we are using JSR validators in our application to validate user input, we have configured the messages to be shown to user in case of validation failures. shown below is message.properties file:
NotEmpty.user.firstName=First name can not be blank. NotEmpty.user.lastName=Last name can not be blank. NotEmpty.user.email=Email can not be blank. NotEmpty.user.password=Password can not be blank. NotEmpty.user.ssoId=SSO ID can not be blank. NotEmpty.user.userProfiles=At least one profile must be selected. non.unique.ssoId=SSO ID {0} already exist. Please fill in different value.
Finally, the Spring Intializer class is shown below:
package com.websystique.springmvc.configuration; import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer; public class AppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer { @Override protected Class<?>[] getRootConfigClasses() { return new Class[] { AppConfig.class }; } @Override protected Class<?>[] getServletConfigClasses() { return null; } @Override protected String[] getServletMappings() { return new String[] { "/" }; } }
Step 6: Create Spring Controller
package com.websystique.springmvc.controller; import java.util.List; import java.util.Locale; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.validation.Valid; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.MessageSource; import org.springframework.security.authentication.AuthenticationTrustResolver; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.web.authentication.rememberme.PersistentTokenBasedRememberMeServices; import org.springframework.stereotype.Controller; import org.springframework.ui.ModelMap; import org.springframework.validation.BindingResult; import org.springframework.validation.FieldError; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.SessionAttributes; import com.websystique.springmvc.model.User; import com.websystique.springmvc.model.UserProfile; import com.websystique.springmvc.service.UserProfileService; import com.websystique.springmvc.service.UserService; @Controller @RequestMapping("/") @SessionAttributes("roles") public class AppController { @Autowired UserService userService; @Autowired UserProfileService userProfileService; @Autowired MessageSource messageSource; @Autowired PersistentTokenBasedRememberMeServices persistentTokenBasedRememberMeServices; @Autowired AuthenticationTrustResolver authenticationTrustResolver; /** * This method will list all existing users. */ @RequestMapping(value = { "/", "/list" }, method = RequestMethod.GET) public String listUsers(ModelMap model) { List<User> users = userService.findAllUsers(); model.addAttribute("users", users); model.addAttribute("loggedinuser", getPrincipal()); return "userslist"; } /** * This method will provide the medium to add a new user. */ @RequestMapping(value = { "/newuser" }, method = RequestMethod.GET) public String newUser(ModelMap model) { User user = new User(); model.addAttribute("user", user); model.addAttribute("edit", false); model.addAttribute("loggedinuser", getPrincipal()); return "registration"; } /** * This method will be called on form submission, handling POST request for * saving user in database. It also validates the user input */ @RequestMapping(value = { "/newuser" }, method = RequestMethod.POST) public String saveUser(@Valid User user, BindingResult result, ModelMap model) { if (result.hasErrors()) { return "registration"; } /* * Preferred way to achieve uniqueness of field [sso] should be implementing custom @Unique annotation * and applying it on field [sso] of Model class [User]. * * Below mentioned peace of code [if block] is to demonstrate that you can fill custom errors outside the validation * framework as well while still using internationalized messages. * */ if(!userService.isUserSSOUnique(user.getId(), user.getSsoId())){ FieldError ssoError =new FieldError("user","ssoId",messageSource.getMessage("non.unique.ssoId", new String[]{user.getSsoId()}, Locale.getDefault())); result.addError(ssoError); return "registration"; } userService.saveUser(user); model.addAttribute("success", "User " + user.getFirstName() + " "+ user.getLastName() + " registered successfully"); model.addAttribute("loggedinuser", getPrincipal()); //return "success"; return "registrationsuccess"; } /** * This method will provide the medium to update an existing user. */ @RequestMapping(value = { "/edit-user-{ssoId}" }, method = RequestMethod.GET) public String editUser(@PathVariable String ssoId, ModelMap model) { User user = userService.findBySSO(ssoId); model.addAttribute("user", user); model.addAttribute("edit", true); model.addAttribute("loggedinuser", getPrincipal()); return "registration"; } /** * This method will be called on form submission, handling POST request for * updating user in database. It also validates the user input */ @RequestMapping(value = { "/edit-user-{ssoId}" }, method = RequestMethod.POST) public String updateUser(@Valid User user, BindingResult result, ModelMap model, @PathVariable String ssoId) { if (result.hasErrors()) { return "registration"; } /*//Uncomment below 'if block' if you WANT TO ALLOW UPDATING SSO_ID in UI which is a unique key to a User. if(!userService.isUserSSOUnique(user.getId(), user.getSsoId())){ FieldError ssoError =new FieldError("user","ssoId",messageSource.getMessage("non.unique.ssoId", new String[]{user.getSsoId()}, Locale.getDefault())); result.addError(ssoError); return "registration"; }*/ userService.updateUser(user); model.addAttribute("success", "User " + user.getFirstName() + " "+ user.getLastName() + " updated successfully"); model.addAttribute("loggedinuser", getPrincipal()); return "registrationsuccess"; } /** * This method will delete an user by it's SSOID value. */ @RequestMapping(value = { "/delete-user-{ssoId}" }, method = RequestMethod.GET) public String deleteUser(@PathVariable String ssoId) { userService.deleteUserBySSO(ssoId); return "redirect:/list"; } /** * This method will provide UserProfile list to views */ @ModelAttribute("roles") public List<UserProfile> initializeProfiles() { return userProfileService.findAll(); } /** * This method handles Access-Denied redirect. */ @RequestMapping(value = "/Access_Denied", method = RequestMethod.GET) public String accessDeniedPage(ModelMap model) { model.addAttribute("loggedinuser", getPrincipal()); return "accessDenied"; } /** * This method handles login GET requests. * If users is already logged-in and tries to goto login page again, will be redirected to list page. */ @RequestMapping(value = "/login", method = RequestMethod.GET) public String loginPage() { if (isCurrentAuthenticationAnonymous()) { return "login"; } else { return "redirect:/list"; } } /** * This method handles logout requests. * Toggle the handlers if you are RememberMe functionality is useless in your app. */ @RequestMapping(value="/logout", method = RequestMethod.GET) public String logoutPage (HttpServletRequest request, HttpServletResponse response){ Authentication auth = SecurityContextHolder.getContext().getAuthentication(); if (auth != null){ //new SecurityContextLogoutHandler().logout(request, response, auth); persistentTokenBasedRememberMeServices.logout(request, response, auth); SecurityContextHolder.getContext().setAuthentication(null); } return "redirect:/login?logout"; } /** * This method returns the principal[user-name] of logged-in user. */ private String getPrincipal(){ String userName = null; Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal(); if (principal instanceof UserDetails) { userName = ((UserDetails)principal).getUsername(); } else { userName = principal.toString(); } return userName; } /** * This method returns true if users is already authenticated [logged-in], else false. */ private boolean isCurrentAuthenticationAnonymous() { final Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); return authenticationTrustResolver.isAnonymous(authentication); } }
This is a trivial Spring MVC controller. Comments on Each method provide the explanations.
Step 7: Create Models
package com.websystique.springmvc.model; import java.io.Serializable; import java.util.HashSet; import java.util.Set; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.FetchType; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; import javax.persistence.JoinColumn; import javax.persistence.JoinTable; import javax.persistence.ManyToMany; import javax.persistence.Table; import org.hibernate.validator.constraints.NotEmpty; @Entity @Table(name="APP_USER") public class User implements Serializable{ @Id @GeneratedValue(strategy=GenerationType.IDENTITY) private Integer id; @NotEmpty @Column(name="SSO_ID", unique=true, nullable=false) private String ssoId; @NotEmpty @Column(name="PASSWORD", nullable=false) private String password; @NotEmpty @Column(name="FIRST_NAME", nullable=false) private String firstName; @NotEmpty @Column(name="LAST_NAME", nullable=false) private String lastName; @NotEmpty @Column(name="EMAIL", nullable=false) private String email; @NotEmpty @ManyToMany(fetch = FetchType.LAZY) @JoinTable(name = "APP_USER_USER_PROFILE", joinColumns = { @JoinColumn(name = "USER_ID") }, inverseJoinColumns = { @JoinColumn(name = "USER_PROFILE_ID") }) private Set<UserProfile> userProfiles = new HashSet<UserProfile>(); public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public String getSsoId() { return ssoId; } public void setSsoId(String ssoId) { this.ssoId = ssoId; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } public String getFirstName() { return firstName; } public void setFirstName(String firstName) { this.firstName = firstName; } public String getLastName() { return lastName; } public void setLastName(String lastName) { this.lastName = lastName; } public String getEmail() { return email; } public void setEmail(String email) { this.email = email; } public Set<UserProfile> getUserProfiles() { return userProfiles; } public void setUserProfiles(Set<UserProfile> userProfiles) { this.userProfiles = userProfiles; } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((id == null) ? 0 : id.hashCode()); result = prime * result + ((ssoId == null) ? 0 : ssoId.hashCode()); return result; } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (!(obj instanceof User)) return false; User other = (User) obj; if (id == null) { if (other.id != null) return false; } else if (!id.equals(other.id)) return false; if (ssoId == null) { if (other.ssoId != null) return false; } else if (!ssoId.equals(other.ssoId)) return false; return true; } /* * DO-NOT-INCLUDE passwords in toString function. * It is done here just for convenience purpose. */ @Override public String toString() { return "User [id=" + id + ", ssoId=" + ssoId + ", password=" + password + ", firstName=" + firstName + ", lastName=" + lastName + ", email=" + email + "]"; } }
package com.websystique.springmvc.model; import java.io.Serializable; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; import javax.persistence.Table; @Entity @Table(name="USER_PROFILE") public class UserProfile implements Serializable{ @Id @GeneratedValue(strategy=GenerationType.IDENTITY) private Integer id; @Column(name="TYPE", length=15, unique=true, nullable=false) private String type = UserProfileType.USER.getUserProfileType(); public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public String getType() { return type; } public void setType(String type) { this.type = type; } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((id == null) ? 0 : id.hashCode()); result = prime * result + ((type == null) ? 0 : type.hashCode()); return result; } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (!(obj instanceof UserProfile)) return false; UserProfile other = (UserProfile) obj; if (id == null) { if (other.id != null) return false; } else if (!id.equals(other.id)) return false; if (type == null) { if (other.type != null) return false; } else if (!type.equals(other.type)) return false; return true; } @Override public String toString() { return "UserProfile [id=" + id + ", type=" + type + "]"; } }
package com.websystique.springmvc.model; import java.io.Serializable; public enum UserProfileType implements Serializable{ USER("USER"), DBA("DBA"), ADMIN("ADMIN"); String userProfileType; private UserProfileType(String userProfileType){ this.userProfileType = userProfileType; } public String getUserProfileType(){ return userProfileType; } }
Step 7: Create DAOs
package com.websystique.springmvc.dao; import java.io.Serializable; import java.lang.reflect.ParameterizedType; import org.hibernate.Criteria; import org.hibernate.Session; import org.hibernate.SessionFactory; import org.springframework.beans.factory.annotation.Autowired; public abstract class AbstractDao<PK extends Serializable, T> { private final Class<T> persistentClass; @SuppressWarnings("unchecked") public AbstractDao(){ this.persistentClass =(Class<T>) ((ParameterizedType) this.getClass().getGenericSuperclass()).getActualTypeArguments()[1]; } @Autowired private SessionFactory sessionFactory; protected Session getSession(){ return sessionFactory.getCurrentSession(); } @SuppressWarnings("unchecked") public T getByKey(PK key) { return (T) getSession().get(persistentClass, key); } public void persist(T entity) { getSession().persist(entity); } public void update(T entity) { getSession().update(entity); } public void delete(T entity) { getSession().delete(entity); } protected Criteria createEntityCriteria(){ return getSession().createCriteria(persistentClass); } }
package com.websystique.springmvc.dao; import java.util.List; import com.websystique.springmvc.model.User; public interface UserDao { User findById(int id); User findBySSO(String sso); void save(User user); void deleteBySSO(String sso); List<User> findAllUsers(); }
package com.websystique.springmvc.dao; import java.util.List; import org.hibernate.Criteria; import org.hibernate.Hibernate; import org.hibernate.criterion.Order; import org.hibernate.criterion.Restrictions; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Repository; import com.websystique.springmvc.model.User; @Repository("userDao") public class UserDaoImpl extends AbstractDao<Integer, User> implements UserDao { static final Logger logger = LoggerFactory.getLogger(UserDaoImpl.class); public User findById(int id) { User user = getByKey(id); if(user!=null){ Hibernate.initialize(user.getUserProfiles()); } return user; } public User findBySSO(String sso) { logger.info("SSO : {}", sso); Criteria crit = createEntityCriteria(); crit.add(Restrictions.eq("ssoId", sso)); User user = (User)crit.uniqueResult(); if(user!=null){ Hibernate.initialize(user.getUserProfiles()); } return user; } @SuppressWarnings("unchecked") public List<User> findAllUsers() { Criteria criteria = createEntityCriteria().addOrder(Order.asc("firstName")); criteria.setResultTransformer(Criteria.DISTINCT_ROOT_ENTITY);//To avoid duplicates. List<User> users = (List<User>) criteria.list(); // No need to fetch userProfiles since we are not showing them on list page. Let them lazy load. // Uncomment below lines for eagerly fetching of userProfiles if you want. /* for(User user : users){ Hibernate.initialize(user.getUserProfiles()); }*/ return users; } public void save(User user) { persist(user); } public void deleteBySSO(String sso) { Criteria crit = createEntityCriteria(); crit.add(Restrictions.eq("ssoId", sso)); User user = (User)crit.uniqueResult(); delete(user); } }
package com.websystique.springmvc.dao; import java.util.List; import com.websystique.springmvc.model.UserProfile; public interface UserProfileDao { List<UserProfile> findAll(); UserProfile findByType(String type); UserProfile findById(int id); }
package com.websystique.springmvc.dao; import java.util.List; import org.hibernate.Criteria; import org.hibernate.criterion.Order; import org.hibernate.criterion.Restrictions; import org.springframework.stereotype.Repository; import com.websystique.springmvc.model.UserProfile; @Repository("userProfileDao") public class UserProfileDaoImpl extends AbstractDao<Integer, UserProfile>implements UserProfileDao{ public UserProfile findById(int id) { return getByKey(id); } public UserProfile findByType(String type) { Criteria crit = createEntityCriteria(); crit.add(Restrictions.eq("type", type)); return (UserProfile) crit.uniqueResult(); } @SuppressWarnings("unchecked") public List<UserProfile> findAll(){ Criteria crit = createEntityCriteria(); crit.addOrder(Order.asc("type")); return (List<UserProfile>)crit.list(); } }
Step 8: Create Services
package com.websystique.springmvc.service; import java.util.List; import com.websystique.springmvc.model.User; public interface UserService { User findById(int id); User findBySSO(String sso); void saveUser(User user); void updateUser(User user); void deleteUserBySSO(String sso); List<User> findAllUsers(); boolean isUserSSOUnique(Integer id, String sso); }
package com.websystique.springmvc.service; import java.util.List; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import com.websystique.springmvc.dao.UserDao; import com.websystique.springmvc.model.User; @Service("userService") @Transactional public class UserServiceImpl implements UserService{ @Autowired private UserDao dao; @Autowired private PasswordEncoder passwordEncoder; public User findById(int id) { return dao.findById(id); } public User findBySSO(String sso) { User user = dao.findBySSO(sso); return user; } public void saveUser(User user) { user.setPassword(passwordEncoder.encode(user.getPassword())); dao.save(user); } /* * Since the method is running with Transaction, No need to call hibernate update explicitly. * Just fetch the entity from db and update it with proper values within transaction. * It will be updated in db once transaction ends. */ public void updateUser(User user) { User entity = dao.findById(user.getId()); if(entity!=null){ entity.setSsoId(user.getSsoId()); if(!user.getPassword().equals(entity.getPassword())){ entity.setPassword(passwordEncoder.encode(user.getPassword())); } entity.setFirstName(user.getFirstName()); entity.setLastName(user.getLastName()); entity.setEmail(user.getEmail()); entity.setUserProfiles(user.getUserProfiles()); } } public void deleteUserBySSO(String sso) { dao.deleteBySSO(sso); } public List<User> findAllUsers() { return dao.findAllUsers(); } public boolean isUserSSOUnique(Integer id, String sso) { User user = findBySSO(sso); return ( user == null || ((id != null) && (user.getId() == id))); } }
package com.websystique.springmvc.service; import java.util.List; import com.websystique.springmvc.model.UserProfile; public interface UserProfileService { UserProfile findById(int id); UserProfile findByType(String type); List<UserProfile> findAll(); }
package com.websystique.springmvc.service; import java.util.List; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import com.websystique.springmvc.dao.UserProfileDao; import com.websystique.springmvc.model.UserProfile; @Service("userProfileService") @Transactional public class UserProfileServiceImpl implements UserProfileService{ @Autowired UserProfileDao dao; public UserProfile findById(int id) { return dao.findById(id); } public UserProfile findByType(String type){ return dao.findByType(type); } public List<UserProfile> findAll() { return dao.findAll(); } }
Step 9: Create Views
Start with login page,asking username & password, and optionally ‘RememberMe’ flag.
WEB-INF/views/login.jsp
<%@ page language="java" contentType="text/html; charset=ISO-8859-1" pageEncoding="ISO-8859-1"%> <%@ page isELIgnored="false" %> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1"> <title>Login page</title> <link href="<c:url value='/static/css/bootstrap.css' />" rel="stylesheet"></link> <link href="<c:url value='/static/css/app.css' />" rel="stylesheet"></link> <link rel="stylesheet" type="text/css" href="//cdnjs.cloudflare.com/ajax/libs/font-awesome/4.2.0/css/font-awesome.css" /> </head> <body> <div id="mainWrapper"> <div class="login-container"> <div class="login-card"> <div class="login-form"> <c:url var="loginUrl" value="/login" /> <form action="${loginUrl}" method="post" class="form-horizontal"> <c:if test="${param.error != null}"> <div class="alert alert-danger"> <p>Invalid username and password.</p> </div> </c:if> <c:if test="${param.logout != null}"> <div class="alert alert-success"> <p>You have been logged out successfully.</p> </div> </c:if> <div class="input-group input-sm"> <label class="input-group-addon" for="username"><i class="fa fa-user"></i></label> <input type="text" class="form-control" id="username" name="ssoId" placeholder="Enter Username" required> </div> <div class="input-group input-sm"> <label class="input-group-addon" for="password"><i class="fa fa-lock"></i></label> <input type="password" class="form-control" id="password" name="password" placeholder="Enter Password" required> </div> <div class="input-group input-sm"> <div class="checkbox"> <label><input type="checkbox" id="rememberme" name="remember-me"> Remember Me</label> </div> </div> <input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}" /> <div class="form-actions"> <input type="submit" class="btn btn-block btn-primary btn-default" value="Log in"> </div> </form> </div> </div> </div> </div> </body> </html>
Once the user is logged-in successfully, he will be presented with list page, showing all existing users. Pay special attentions to Spring Security tags usage below. Add, Edit & Delete links/buttons are shown based on roles only, so a user with ‘User’ role will not even be able to see them. You may ask: but what about directly typing the url in browser-bar? Well, we have already secured the URL’s in Spring Security configuration, so no-worries.
WEB-INF/views/userslist.jsp
<%@ page language="java" contentType="text/html; charset=ISO-8859-1" pageEncoding="ISO-8859-1"%> <%@ page isELIgnored="false" %> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <%@ taglib prefix="sec" uri="http://www.springframework.org/security/tags"%> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1"> <title>Users List</title> <link href="<c:url value='/static/css/bootstrap.css' />" rel="stylesheet"></link> <link href="<c:url value='/static/css/app.css' />" rel="stylesheet"></link> </head> <body> <div class="generic-container"> <%@include file="authheader.jsp" %> <div class="panel panel-default"> <!-- Default panel contents --> <div class="panel-heading"><span class="lead">List of Users </span></div> <table class="table table-hover"> <thead> <tr> <th>Firstname</th> <th>Lastname</th> <th>Email</th> <th>SSO ID</th> <sec:authorize access="hasRole('ADMIN') or hasRole('DBA')"> <th width="100"></th> </sec:authorize> <sec:authorize access="hasRole('ADMIN')"> <th width="100"></th> </sec:authorize> </tr> </thead> <tbody> <c:forEach items="${users}" var="user"> <tr> <td>${user.firstName}</td> <td>${user.lastName}</td> <td>${user.email}</td> <td>${user.ssoId}</td> <sec:authorize access="hasRole('ADMIN') or hasRole('DBA')"> <td><a href="<c:url value='/edit-user-${user.ssoId}' />" class="btn btn-success custom-width">edit</a></td> </sec:authorize> <sec:authorize access="hasRole('ADMIN')"> <td><a href="<c:url value='/delete-user-${user.ssoId}' />" class="btn btn-danger custom-width">delete</a></td> </sec:authorize> </tr> </c:forEach> </tbody> </table> </div> <sec:authorize access="hasRole('ADMIN')"> <div class="well"> <a href="<c:url value='/newuser' />">Add New User</a> </div> </sec:authorize> </div> </body> </html>
Above page also includes a jsp containing welcome-messagealong with Logout link as shown below:
WEB-INF/views/authheader.jsp
<div class="authbar"> <span>Dear <strong>${loggedinuser}</strong>, Welcome to CrazyUsers.</span> <span class="floatRight"><a href="<c:url value="/logout" />">Logout</a></span> </div>
A user with ‘Admin’ role can add a new user. Shown below is the registration page for the same.
WEB-INF/views/registration.jsp
<%@ page language="java" contentType="text/html; charset=ISO-8859-1" pageEncoding="ISO-8859-1"%> <%@ page isELIgnored="false" %> <%@ taglib prefix="form" uri="http://www.springframework.org/tags/form"%> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1"> <title>User Registration Form</title> <link href="<c:url value='/static/css/bootstrap.css' />" rel="stylesheet"></link> <link href="<c:url value='/static/css/app.css' />" rel="stylesheet"></link> </head> <body> <div class="generic-container"> <%@include file="authheader.jsp" %> <div class="well lead">User Registration Form</div> <form:form method="POST" modelAttribute="user" class="form-horizontal"> <form:input type="hidden" path="id" id="id"/> <div class="row"> <div class="form-group col-md-12"> <label class="col-md-3 control-lable" for="firstName">First Name</label> <div class="col-md-7"> <form:input type="text" path="firstName" id="firstName" class="form-control input-sm"/> <div class="has-error"> <form:errors path="firstName" class="help-inline"/> </div> </div> </div> </div> <div class="row"> <div class="form-group col-md-12"> <label class="col-md-3 control-lable" for="lastName">Last Name</label> <div class="col-md-7"> <form:input type="text" path="lastName" id="lastName" class="form-control input-sm" /> <div class="has-error"> <form:errors path="lastName" class="help-inline"/> </div> </div> </div> </div> <div class="row"> <div class="form-group col-md-12"> <label class="col-md-3 control-lable" for="ssoId">SSO ID</label> <div class="col-md-7"> <c:choose> <c:when test="${edit}"> <form:input type="text" path="ssoId" id="ssoId" class="form-control input-sm" disabled="true"/> </c:when> <c:otherwise> <form:input type="text" path="ssoId" id="ssoId" class="form-control input-sm" /> <div class="has-error"> <form:errors path="ssoId" class="help-inline"/> </div> </c:otherwise> </c:choose> </div> </div> </div> <div class="row"> <div class="form-group col-md-12"> <label class="col-md-3 control-lable" for="password">Password</label> <div class="col-md-7"> <form:input type="password" path="password" id="password" class="form-control input-sm" /> <div class="has-error"> <form:errors path="password" class="help-inline"/> </div> </div> </div> </div> <div class="row"> <div class="form-group col-md-12"> <label class="col-md-3 control-lable" for="email">Email</label> <div class="col-md-7"> <form:input type="text" path="email" id="email" class="form-control input-sm" /> <div class="has-error"> <form:errors path="email" class="help-inline"/> </div> </div> </div> </div> <div class="row"> <div class="form-group col-md-12"> <label class="col-md-3 control-lable" for="userProfiles">Roles</label> <div class="col-md-7"> <form:select path="userProfiles" items="${roles}" multiple="true" itemValue="id" itemLabel="type" class="form-control input-sm" /> <div class="has-error"> <form:errors path="userProfiles" class="help-inline"/> </div> </div> </div> </div> <div class="row"> <div class="form-actions floatRight"> <c:choose> <c:when test="${edit}"> <input type="submit" value="Update" class="btn btn-primary btn-sm"/> or <a href="<c:url value='/list' />">Cancel</a> </c:when> <c:otherwise> <input type="submit" value="Register" class="btn btn-primary btn-sm"/> or <a href="<c:url value='/list' />">Cancel</a> </c:otherwise> </c:choose> </div> </div> </form:form> </div> </body> </html>
WEB-INF/views/registrationsuccess.jsp
<%@ page language="java" contentType="text/html; charset=ISO-8859-1" pageEncoding="ISO-8859-1"%> <%@ page isELIgnored="false" %> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1"> <title>Registration Confirmation Page</title> <link href="<c:url value='/static/css/bootstrap.css' />" rel="stylesheet"></link> <link href="<c:url value='/static/css/app.css' />" rel="stylesheet"></link> </head> <body> <div class="generic-container"> <%@include file="authheader.jsp" %> <div class="alert alert-success lead"> ${success} </div> <span class="well floatRight"> Go to <a href="<c:url value='/list' />">Users List</a> </span> </div> </body> </html>
AccessDenied page will be shown if the users is not allowed to go to certain url’s.
WEB-INF/views/accessDenied.jsp
<%@ page language="java" contentType="text/html; charset=ISO-8859-1" pageEncoding="ISO-8859-1"%> <%@ page isELIgnored="false" %> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1"> <title>AccessDenied page</title> </head> <body> <div class="generic-container"> <div class="authbar"> <span>Dear <strong>${loggedinuser}</strong>, You are not authorized to access this page.</span> <span class="floatRight"><a href="<c:url value="/logout" />">Logout</a></span> </div> </div> </body> </html>
Step 10: Create and populate schema in database
/*All User's gets stored in APP_USER table*/ create table APP_USER ( id BIGINT NOT NULL AUTO_INCREMENT, sso_id VARCHAR(30) NOT NULL, password VARCHAR(100) NOT NULL, first_name VARCHAR(30) NOT NULL, last_name VARCHAR(30) NOT NULL, email VARCHAR(30) NOT NULL, PRIMARY KEY (id), UNIQUE (sso_id) ); /* USER_PROFILE table contains all possible roles */ create table USER_PROFILE( id BIGINT NOT NULL AUTO_INCREMENT, type VARCHAR(30) NOT NULL, PRIMARY KEY (id), UNIQUE (type) ); /* JOIN TABLE for MANY-TO-MANY relationship*/ CREATE TABLE APP_USER_USER_PROFILE ( user_id BIGINT NOT NULL, user_profile_id BIGINT NOT NULL, PRIMARY KEY (user_id, user_profile_id), CONSTRAINT FK_APP_USER FOREIGN KEY (user_id) REFERENCES APP_USER (id), CONSTRAINT FK_USER_PROFILE FOREIGN KEY (user_profile_id) REFERENCES USER_PROFILE (id) ); /* Populate USER_PROFILE Table */ INSERT INTO USER_PROFILE(type) VALUES ('USER'); INSERT INTO USER_PROFILE(type) VALUES ('ADMIN'); INSERT INTO USER_PROFILE(type) VALUES ('DBA'); /* Populate one Admin User which will further create other users for the application using GUI */ INSERT INTO APP_USER(sso_id, password, first_name, last_name, email) VALUES ('sam','$2a$10$4eqIF5s/ewJwHK1p8lqlFOEm2QIA0S8g6./Lok.pQxqcxaBZYChRm', 'Sam','Smith','samy@xyz.com'); /* Populate JOIN Table */ INSERT INTO APP_USER_USER_PROFILE (user_id, user_profile_id) SELECT user.id, profile.id FROM app_user user, user_profile profile where user.sso_id='sam' and profile.type='ADMIN'; /* Create persistent_logins Table used to store rememberme related stuff*/ CREATE TABLE persistent_logins ( username VARCHAR(64) NOT NULL, series VARCHAR(64) NOT NULL, token VARCHAR(64) NOT NULL, last_used TIMESTAMP NOT NULL, PRIMARY KEY (series) );
Note that we have inserted one user manually(we do need one Admin user to actually login and create further users for application). This is a real-world scenario. Notice the password which is encrypted form of password ‘abc125’. It’s generated using below mentioned utility class [it could even have been a script] which is used only and only to generate a password for one initial Admin user. It can well be removed from application.
package com.websystique.springsecurity.util; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; public class QuickPasswordEncodingGenerator { /** * @param args */ public static void main(String[] args) { String password = "abc125"; BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder(); System.out.println(passwordEncoder.encode(password)); } }
Step 11: Build, deploy and Run Application
Now build the war (either by eclipse as was mentioned in previous tutorials) or via maven command line( mvn clean install
). Deploy the war to a Servlet 3.0 container . Since here i am using Tomcat, i will simply put this war file into tomcat webapps folder
and click on start.bat
inside tomcat/bin directory.
If you prefer to deploy from within Eclipse using tomcat: For those of us, who prefer to deploy and run from within eclipse, and might be facing difficulties setting Eclipse with tomcat, the detailed step-by-step solution can be found at : How to setup tomcat with Eclipse.
Open browser and browse at http://localhost:8080/SpringMVCHibernateWithSpringSecurityExample/
Login with User Sam & password abc125, check RememberMe as well.
Check database now.An entry should be made in persistent_logins table.
Nothing changes for APP_USER table though.
Now click on ‘Add new user’ link. Add a user with ‘USER’ role.
Click on Register, user should be added.
Click on ‘Users List’ link. You should see the newly added user.
Add another user with DBA & USER role.
Register. Now check the list again.
Verify APP_USER table.
Now logout.
Check persistent_logins table, entry should be removed.
Login with user ‘will’ which has ‘User’ role. No Add/Edit/Delete links are available to this user.
Now logout and login with ‘bob’. No Add/Delete links are available to this user.
Now try to manually type the delete URL in browser-bar and enter.You should see AccessDenied page.
That’s it. As we saw, it’s rather simple to integrate Spring Security with Spring MVC. Feel free to Comment, and suggest improvements.
Download Source Code
References
- Improved Persistent Login Cookie Best Practice
- Spring Security 4 Project Page
- Spring Security 4 Reference Manual
- Spring 4 Reference Manual
If you like tutorials on this site, why not take a step further and connect me on Facebook , Google Plus & Twitter as well? I would love to hear your thoughts on these articles, it will help improve further our learning process.