Monday 30 December 2013

Spring Security: Custom UserDetails Service and Login form

To get familiar with key concepts of Spring Security, refer to my previous post. In this post, we will be writing a custom UserDetails service which will talk to the database [hibernate + MySQL] and fetch the user authentication information.

Setup

This is how my project structure looks like:

Dependencies

If you are using maven, following is the pom.xml file containing the list of all the dependencies:

 4.0.0
 SpringSecurity
 SpringSecurity1
 war
 0.0.1-SNAPSHOT
 SpringSecurity1 Maven Webapp
 http://maven.apache.org
 
  
   junit
   junit
   3.8.1
   test
  
  
   org.springframework
   spring-orm
   3.2.0.RELEASE
  
  
   org.springframework
   spring-webmvc
   3.2.0.RELEASE
  
  
   org.springframework.security
   spring-security-web
   3.2.0.RELEASE
  
  
   org.springframework.security
   spring-security-config
   3.2.0.RELEASE
  
  
   org.springframework.security
   spring-security-taglibs
   3.2.0.RELEASE
  
  
   jstl
   jstl
   1.2
   compile
  
  
   taglibs
   standard
   1.1.2
   compile
  
  
   javax
   javaee-api
   7.0
  
  
   org.hibernate
   hibernate-core
   3.6.10.Final
  
  
   mysql
   mysql-connector-java
   5.1.26
  
  
   commons-dbcp
   commons-dbcp
   20030825.184428
  
  
   commons-pool
   commons-pool
   20030825.183949
  
  
   commons-collections
   commons-collections
   3.2.1
  
  
   javassist
   javassist
   3.12.1.GA
  
  
   org.codehaus.jackson
   jackson-mapper-asl
   1.9.12
  
 
 
  SpringSecurity1
 



Configuring web.xml


  SpringSecurity1
  
    /WEB-INF/jsp/index.jsp
  
  
    spring
    
            org.springframework.web.servlet.DispatcherServlet
        
    1
  
  
    spring
    /
  
  
    
                  org.springframework.web.context.ContextLoaderListener
                
  
  
    contextConfigLocation
    
   /WEB-INF/spring-servlet.xml,
   /WEB-INF/spring-security.xml
  
  
  
    springSecurityFilterChain
    
                  org.springframework.web.filter.DelegatingFilterProxy
                
  
  
    springSecurityFilterChain
    /*
  

This provides a hook into the Spring Security web infrastructure. DelegatingFilterProxy is a Spring Framework class which delegates to a filter implementation which is defined as a Spring bean in your application context. In this case, the bean is named "springSecurityFilterChain", which is an internal infrastructure bean created by the namespace to handle web security. Note that you should not use this bean name yourself. Once you’ve added this to your web.xml, you’re ready to start editing your application context file. Web security services are configured using the <http> element.

Configuring spring-security.xml



 
  
  
   
  
 

 
  
  
 


The <intercept-url> element defines a pattern which is matched against the URLs of incoming requests using an ant path style syntax. In general the xml states that we want to log in to the application using a form with username and password, and that we want a logout URL registered which will allow us to log out of the application.The access attribute defines the access requirements for requests matching the given pattern. With the default configuration, this is typically a comma-separated list of roles, one of which a user must have to be allowed to make the request.Here, it states that we want all URLs matching the pattern '/user/**' to be secured, requiring the role ROLE_USER to access them.Similarly, we want all URls matching the pattern '/admin/**' require the role ROLE_ADMIN to access them.

If a form login isn't prompted by an attempt to access a protected resource, the default-target-url option comes into play. This is the URL the user will be taken to after successfully logging in, and defaults to "/". One can also configure things so that the user always ends up at this page (regardless of whether the login was "on-demand" or they explicitly chose to log in) by setting the always-use-default-target attribute to "true". The authentication-failure-attribute defines the URL that  shows the custom access denied page if the user fails to authenticate himself.

The logout-success-url specifies the URL which will be presented to the user once the user has logged out.

Authentication manager will handle all the authentication requests. All authentication-provider elements must be children of the <authentication-manager> element, which creates a ProviderManager and registers the authentication providers with it. The user-service-ref attribute for the authentication provider says that we have a custom implementation of Spring Security’s UserDetailsService, called "authService" defined in the application context file. UserDetailsService is a special interface which has the only method which accepts a String-based username argument and returns a UserDetails.

UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;


UserDetails is a core interface in Spring Security which represents a principal, but in an extensible and application-specific way. Think of UserDetails as the adapter between your own user database and what Spring Security needs inside the SecurityContextHolder. The returned UserDetails is an interface that provides getters that guarantee non-null provision of authentication information such as the username, password, granted authorities and whether the user account is enabled or disabled.

Note that UserDetailsService is purely a DAO for user data and performs no other function other than to supply that data to other components within the framework. In particular, it does not authenticate the user, which is done by the AuthenticationManager.

Configuring spring-servlet.xml




 

 

 
  
   
    
   
  
 

 
  
  
 

 
  
  
  
  
 

 
  
  
   classpath:hibernate.cfg.xml
  
  
   org.hibernate.cfg.AnnotationConfiguration
   
  
 

 

 

 
  
 

Here, we have defined a bean authService which implements the interface UserDetailsService. It also has hibernate configurations as we will be storing the user authentication information in my database [MySQL]. 

hibernate.cfg.xml contents


 
  
  
  1

  
  org.hibernate.dialect.MySQLDialect

  
  org.hibernate.cache.NoCacheProvider

  
  true

  create
  
  
  
 

MyUser model object

package com.spring.security.domain;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.Table;

@Entity
@Table(name = "userdetails")
public class MyUser {

 @Id
 @GeneratedValue
 private int userId;
 private String username;
 private String password;
 private String role;

 public String getUsername() {
  return username;
 }

 public void setUsername(String username) {
  this.username = username;
 }

 public String getPassword() {
  return password;
 }

 public void setPassword(String password) {
  this.password = password;
 }

 public int getUserId() {
  return userId;
 }

 public void setUserId(int userId) {
  this.userId = userId;
 }

 public String getRole() {
  return role;
 }

 public void setRole(String role) {
  this.role = role;
 }

}
It has username, password and roles attributes which will be stored in database.

UserDetailsService Implementation

package com.spring.security.service.impl;

import java.util.ArrayList;
import java.util.Collection;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
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.spring.security.dao.IUserDao;
import com.spring.security.domain.MyUser;
import com.spring.security.service.IAuthService;

@Service
public class AuthServiceImpl implements IAuthService, UserDetailsService {

 @Autowired
 IUserDao userDao;

 @Transactional
 @Override
 public UserDetails loadUserByUsername(String username)
   throws UsernameNotFoundException {

  MyUser details = userDao.getUser(username);
  Collection authorities = new ArrayList();
  SimpleGrantedAuthority userAuthority = new SimpleGrantedAuthority(
    "ROLE_USER");
  SimpleGrantedAuthority adminAuthority = new SimpleGrantedAuthority(
    "ROLE_ADMIN");
  if (details.getRole().equals("user"))
   authorities.add(userAuthority);
  else if (details.getRole().equals("admin")) {
   authorities.add(userAuthority);
   authorities.add(adminAuthority);
  }
  UserDetails user = new User(details.getUsername(),
    details.getPassword(), true, true, true, true, authorities);
  return user;
 }
}

As u can see, AuthServiceImpl implements UserDetailsService interface. This needs to override a loadUserByUsername() method that takes username as an argument and returns a UserDetails object. We fetch a MyUser object from DB that stores the username,password and role for a user. It checks if role present is 'user' it adds a ROLE_USER authority to the authorities list. If it is 'admin' it adds ROLE_ADMIN as well as ROLE_USER authorities. It then creates a UserDetails object using a constructor passing the appropriate arguments.

Controller

package com.spring.security.controller;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;

import com.spring.security.domain.MyUser;
import com.spring.security.service.IUserService;

@Controller
public class HelloController {

 @Autowired
 IUserService userService;

 @RequestMapping(value = "/user/welcome", method = RequestMethod.GET)
 public String printWelcomeUser() {
  return "hello";
 }

 @RequestMapping(value = "/admin/welcome", method = RequestMethod.GET)
 public String printWelcomeAdmin() {
  return "admin";
 }

 @RequestMapping(value = "/login", method = RequestMethod.GET)
 public String getLoginPage(Model model) {
  return "login";
 }

 @RequestMapping(value = "/home", method = RequestMethod.GET)
 public String getHomePage(Model model) {
  return "hello";
 }

 @RequestMapping(value = "/accessdenied", method = RequestMethod.GET)
 public String getFailurePage(Model model) {
  return "failure";
 }

 @RequestMapping(value = "/logout", method = RequestMethod.GET)
 public String getLogoutPage(Model model, HttpServletRequest req) {
  req.getSession().invalidate();
  return "logout";
 }

 @RequestMapping(value = "/user", method = RequestMethod.POST)
 @ResponseBody
 String saveUser(@RequestBody MyUser user, HttpServletResponse response) {
  System.out.println("User:" + user.getUsername());
  userService.saveUser(user);
  response.setStatus(201);
  return "success";
 }
}

UserDao

package com.spring.security.dao.impl;

import org.hibernate.Criteria;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.criterion.Restrictions;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;

import com.spring.security.dao.IUserDao;
import com.spring.security.domain.MyUser;

@Repository
public class UserDaoImpl implements IUserDao {

 @Autowired
 private SessionFactory sessionFactory;

 @Override
 public MyUser getUser(String username) {
  Session session = sessionFactory.getCurrentSession();
  Criteria criteria = session.createCriteria(MyUser.class);
  criteria.add(Restrictions.eq("username", username));
  MyUser user = (MyUser) criteria.uniqueResult();
  return user;
 }

 @Override
 public void saveUser(MyUser user) {
  sessionFactory.getCurrentSession().save(user);
 }
}


Views

admin.jsp
<%@ page language="java" contentType="text/html; charset=ISO-8859-1"
    pageEncoding="ISO-8859-1"%>




Hello admin


Welcome admin !

Logout
Simple welcome page that displays Welcome Admin !The default logout URL is /j_spring_security_logout, but you can set it to something else using the logout-url attribute in spring-security.xml.

hello.jsp
<%@ page language="java" contentType="text/html; charset=ISO-8859-1"
 pageEncoding="ISO-8859-1"%>




Hello user



 

Welcome User !

Logout
Simple welcome page that displays Welcome User !

index.jsp
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<%@ page language="java" contentType="text/html; charset=ISO-8859-1"
 pageEncoding="ISO-8859-1"%>





Insert title here



 

Hello All

Home page

login.jsp
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%>
<%@ taglib uri="http://www.springframework.org/tags/form" prefix="form"%>
<%@ taglib uri="http://www.springframework.org/tags" prefix="spring"%>



 

Login to Spring Security App

Username:
Password:
 
 
Post request to  /j_spring_security_check authenticates the user. The username parameter is set to j_username and password to j_password. 

logout.jsp
<%@ page language="java" contentType="text/html; charset=ISO-8859-1"
 pageEncoding="ISO-8859-1"%>




Logout


 

You have been successfully logged out !

Go back to login page
Logout page to which a user is directed after successful log out.

failure.jsp
<%@ page language="java" contentType="text/html; charset=ISO-8859-1"
 pageEncoding="ISO-8859-1"%>




 

Unauthenticated user !!

Access denied! Go back to login page
Access denied page which is shown to the user if he fails to authenticate.


Demo

This is my database state.



When we try to access /user/welcome resource, we will be prompted with our custom login page.




When we log in as user1, we will be granted ROLE_USER authority and hence we will not be able to access /admin/welcome which requires ROLE_ADMIN authority.



If we enter incorrect credentials, we will be presented with our custom access denied web page. 



You can view/download the complete source code from here.

Thanks !!

1 comment: