CustomMenu

Sunday, April 13, 2014

Spring MVC 4 & Spring Security 3.2 - Part 2

We will now convert the example from the last blog post, Spring MVC 4 & Spring Security 3.2 - Part 1, to use a simple Spring Security database schema as well as the UserDetailsService.  We will ignore roles for this post because they will be added in Part 3.

1. We will not be using the default schema for Spring Security, but you can review it here Spring Security Schema if desired.  In the same database already configured for the strategy table, execute the following DDL to create a users table.
CREATE TABLE `users` (
    `id` int(6) NOT NULL AUTO_INCREMENT,  
    `username` VARCHAR(50) NOT NULL UNIQUE,
    `password` VARCHAR(50) NOT NULL,
    `enabled` BOOLEAN NOT NULL,
    PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8; 

2. Add the dummy data to our new database table.  I use HeidiSQL to both create and populate the tables.
INSERT INTO `users` (`username`, `password`, `enabled`) VALUES
    ('admin', 'password', TRUE),
    ('trader', 'password', TRUE),
    ('user', 'password', TRUE);

3. The next step is to update the security configuration.  I've renamed this configuration file SecurityConfig.java from the old name WebSecurityConfig.java.
package com.dtr.oas.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.ComponentScan;
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.WebSecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.servlet.configuration.EnableWebMvcSecurity;
import org.springframework.security.core.userdetails.UserDetailsService;

@Configuration
@EnableWebMvcSecurity
@ComponentScan(basePackageClasses=com.dtr.oas.service.UserServiceImpl.class)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
    @Autowired
    public void configureGlobal(UserDetailsService userDetailsService, AuthenticationManagerBuilder auth) throws Exception {
        auth
            .userDetailsService(userDetailsService);
    }
   
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
                .antMatchers("/resources/**", "/signup").permitAll()
                .anyRequest().authenticated()
                .and()
            .formLogin()
                .loginPage("/login")
                .permitAll()
                .and()
            .logout()
                .permitAll();
    }
}
The changes include the addition of a component scan for our implementation of the UserDetailsService, and changing the configureGlobal() method to use our implementation of the UserDetailsService for authentication.


4. Now that the configuration is complete, we will create the User class:
package com.dtr.oas.model;
import java.util.ArrayList;
import java.util.Collection;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Table;
import javax.persistence.Transient;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import org.hibernate.validator.constraints.NotEmpty;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import com.google.common.base.Objects;

@Entity  
@Table(name="USERS")
public class User extends BaseEntity implements UserDetails {
    private static final long serialVersionUID = 6311364761937265306L;
    static Logger logger = LoggerFactory.getLogger(User.class);
    
    @NotNull(message = "{error.user.username.null}")
    @NotEmpty(message = "{error.user.username.empty}")
    @Size(max = 50, message = "{error.user.username.max}")
    @Column(name = "username", length = 50)
    private String username;

    @NotNull(message = "{error.user.password.null}")
    @NotEmpty(message = "{error.user.password.empty}")
    @Size(max = 50, message = "{error.user.password.max}")
    @Column(name = "password", length = 50)
    private String password;
    
    @Column(name = "enabled")
    private boolean enabled;
    
    @Transient
    private Collection<? extends GrantedAuthority> authorities = new ArrayList<>();
    
    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 boolean getEnabled() {
        return enabled;
    }

    public void setEnabled(boolean enabled) {
        this.enabled = enabled;
    }

    @Override
    public String toString() {
        return String.format("%s(id=%d, username=%s, password=%s, enabled=%b)", 
                this.getClass().getSimpleName(), 
                this.getId(), 
                this.getUsername(), 
                this.getPassword(), 
                this.getEnabled());
    }

    @Override
    public boolean equals(Object o) {
        if (this == o)
            return true;
        if (o == null)
            return false;

        if (o instanceof User) {
            final User other = (User) o;
            return Objects.equal(getId(), other.getId())
                    && Objects.equal(getUsername(), other.getUsername())
                    && Objects.equal(getPassword(), other.getPassword())
                    && Objects.equal(getEnabled(), other.getEnabled());
        }
        return false;
    }

    @Override
    public int hashCode() {
        return Objects.hashCode(getId(), getUsername(), getPassword(), getEnabled());
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return this.authorities;
    }
    
    //TODO temporary authorities implementation - will revise in the next example
    public void setAuthorities(Collection<? extends GrantedAuthority> authorities) {
        this.authorities = authorities;
    }

    @Override
    public boolean isAccountNonExpired() {
        //return true = account is valid / not expired
        return true; 
    }

    @Override
    public boolean isAccountNonLocked() {
        //return true = account is not locked
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        //return true = password is valid / not expired
        return true;
    }

    @Override
    public boolean isEnabled() {
        return this.getEnabled();
    }
    
}
There are several points to note in the above class:
  1. This class extends our BaseEntity class that provides the auto incremented id value
  2. This class extends Spring Security's UserDetails class that is the expected return type from Spring Security's call to UserDetailsService. This class implements all of the UserDetails methods.
  3. Until we implement roles in the next blog post, we have a @Transient variable called authorities as a placeholder The associated getters and setters will be addressed in the next blog post as well.
  4. The last five methods in this class are required to satisfy the UserDetails extension

5. Now we need to create the DAO layer in order to access the User object. As with the Strategy class, we will need an interface and a class that implements the interface. First the interface, then the implementation:
package com.dtr.oas.dao;
import java.util.List;
import com.dtr.oas.model.User;

public interface UserDAO {

    public void addUser(User user);

    public User getUser(int userId) throws UserNotFoundException;

    public User getUser(String username) throws UserNotFoundException;

    public void updateUser(User user) throws UserNotFoundException;

    public void deleteUser(int userId) throws UserNotFoundException;

    public List<User> getUsers();
}
package com.dtr.oas.dao;
import java.util.List;
import org.hibernate.Query;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
import com.dtr.oas.model.User;

@Repository
public class UserDAOImpl implements UserDAO {
    static Logger logger = LoggerFactory.getLogger(UserDAOImpl.class);

    @Autowired
    private SessionFactory sessionFactory;

    private Session getCurrentSession() {
        return sessionFactory.getCurrentSession();
    }

    @Override
    public void addUser(User user) {
        getCurrentSession().save(user);
    }

    @Override
    public User getUser(int userId) throws UserNotFoundException {
        logger.debug("UserDAOImpl.getUser() - [" + userId + "]");
        User userObject = (User) getCurrentSession().get(User.class, userId);
        
        if (userObject == null) {
            throw new UserNotFoundException("User id [" + userId + "] not found");
        } else {
            return userObject;
        }
    }

    @SuppressWarnings("unchecked")
    @Override
    public User getUser(String usersName) throws UserNotFoundException {
        logger.debug("UserDAOImpl.getUser() - [" + usersName + "]");
        Query query = getCurrentSession().createQuery("from User where username = :usersName ");
        query.setString("usersName", usersName);
        
        logger.debug(query.toString());
        if (query.list().size() == 0 ) {
            throw new UserNotFoundException("User [" + usersName + "] not found");
        } else {
            logger.debug("User List Size: " + query.list().size());
            List<User> list = (List<User>)query.list();
            User userObject = (User) list.get(0);

            return userObject;
        }
    }

    @Override
    public void updateUser(User user) throws UserNotFoundException {
        User userToUpdate = getUser(user.getId());
        userToUpdate.setUsername(user.getUsername());
        userToUpdate.setPassword(user.getPassword());
        userToUpdate.setEnabled(user.getEnabled());
        userToUpdate.setAuthorities(user.getAuthorities());
        getCurrentSession().update(userToUpdate);
    }

    @Override
    public void deleteUser(int userId) throws UserNotFoundException {
        User user = getUser(userId);
        if (user != null) {
            getCurrentSession().delete(user);
        }
    }

    @Override
    @SuppressWarnings("unchecked")
    public List<User> getUsers() {
        return getCurrentSession().createQuery("from User").list();
    }
}
There are several points to note in the above classes:
  1. There are two getUser() methods. One takes the id as the search criteria, and the second variation takes the user name as the search criteria.
  2. Both getUser() methods check for the existence of the specific user. If the user does not exist, then a custom exception, UserNotFoundException, is thrown.
  3. The getUser() method that takes the username attribute requires a Hibernate query. This query uses the name of the class, and the name of the instance variable in the query. The query does not use the name of the table or the names of the table columns. Hibernate is object focused rather than db focused and requires us to refer to class names and instance variables.
  4. Many of these methods will not be used for authentication, but will be used when we create user maintenance CRUD pages later.

6. Next we create the service layer interface and implementation classes. First the interface:
package com.dtr.oas.service;
import java.util.List;
import org.springframework.security.core.userdetails.UserDetailsService;
import com.dtr.oas.dao.UserNotFoundException;
import com.dtr.oas.model.User;

public interface UserService extends UserDetailsService {

    public void addUser(User user);

    public User getUser(int userId) throws UserNotFoundException;

    public User getUser(String username) throws UserNotFoundException;

    public void updateUser(User user) throws UserNotFoundException;

    public void deleteUser(int userId) throws UserNotFoundException;

    public List<User> getUsers();
}
package com.dtr.oas.service;
import java.util.ArrayList;
import java.util.Collection;
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.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.dtr.oas.dao.UserDAO;
import com.dtr.oas.dao.UserNotFoundException;
import com.dtr.oas.model.User;

@Service
@Transactional
public class UserServiceImpl implements UserService {
    static Logger logger = LoggerFactory.getLogger(UserServiceImpl.class);
    
    @Autowired
    private UserDAO userDAO;

    @Override
    public void addUser(User user) {
        userDAO.addUser(user);
    }

    @Override
    public User getUser(int userId) throws UserNotFoundException {
        return userDAO.getUser(userId);
    }

    @Override
    public User getUser(String username) throws UserNotFoundException {
        return userDAO.getUser(username);
    }

    @Override
    public void updateUser(User user) throws UserNotFoundException {
        userDAO.updateUser(user);
    }

    @Override
    public void deleteUser(int userId) throws UserNotFoundException {
        userDAO.deleteUser(userId);
    }

    @Override
    public List<User> getUsers() {
        return userDAO.getUsers();
    }
    
    //TODO Dummy role added temporarily until next example
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        try {
            Collection<GrantedAuthority> authorities = new ArrayList<>();
            SimpleGrantedAuthority authority = new SimpleGrantedAuthority("ROLE_ADMIN");
            authorities.add(authority);

            User userObject = getUser(username);
            userObject.setAuthorities(authorities);
            return userObject;
        } catch (UserNotFoundException e) {
            throw new UsernameNotFoundException(e.getMessage());
        }
    }
}
A few points to note in the above classes:
  1. The UserService interface extends the UserDetailsService interface required by Spring Security.
  2. The UserService interface specifies the standard CRUD operations that were present with the Strategy class.
  3. the UserServiceImpl class implements the methods specified by the UserService (and UserDetailsServic) interface.
  4. The UserServiceImpl class was specified in the component scan in our security configuration class.
  5. The UserServiceImpl class implements the loadUserByUsername required by the UserDetailsService.

7. When we run the application in Eclipse, we should see the same flow of screens that we saw in the last example. First the login screen:

8. Then the landing page:

9. Then the list page (notice the username displayed and the logout button):

10. After the logout button is pressed, we are taken to the login screen with the message that we have been logged out:


If you want more information on this topic, the following pages cover Spring Security using a database:

In the next post, we will add roles to the example.

Code at GitHub: https://github.com/dtr-trading/spring-ex09-security

2 comments:

Unknown said...

Thank you very much for all these tutorial...

btw i found a mistake (i think) in source codes that drove me crazy:
in some pages, jquery-1.11.0.min.js, bootstrap-3.1.1.min.js, jquery.metisMenu.js, sb-admin.js
couldn't be found.

I think you should use data-th-src instead data-th-href

Result:


< scr ipt type="text/javascript" src="../../resources/js/jquery-1.11.0.min.js"
data-th-src="@{/resources/js/jquery-1.11.0.min.js}">< /scr ipt>





< scr ipt type="text/javascript" src="../../resources/js/plugins/metisMenu/jquery.metisMenu.js"
data-th-src="@{/resources/js/plugins/metisMenu/jquery.metisMenu.js}">< /sc ript>


< sc ript type="text/javascript" src="../../resources/js/sb-admin.js"
data-th-src="@{/resources/js/sb-admin.js}">< /sc ript>

Dave R. said...

I think you mentioned this already on another post...

Nice catch.

You are correct that the src tag should be used rather than the href tag. You will notice that nearly all of my Thymeleaf files use the correct src tag.

These typos are isolated to the three fragment files, the login file and the home file. The three fragment files are never served by the web server directly (since they are fragments), but you may have seen issues with the home file.

I'll update the source files when shortly.

Thanks!

Dave

Post a Comment