Sunday, December 9, 2012

Spring MVC (Security): custom authentication manager and login page

In the previous post, we've implemented basic authentication and authorization features, mainly relying on the login page that Spring security generates. However, most of the time, we'll want to have our own login page as well as a custom authentication manager (having all the usernames, passwords, and roles hardcoded in the  web.xml file is definitely not a good solution!). Let's see how we could achieve that. As we use the project from this previous post as starting point, feel free to check out it source code here.


Tools and libraries
  • Eclipse Indigo
  • Spring-core 3.1.1
  • Spring-security 3.1.0
  • Tomcat 7
  • Maven 3
STEP 1 - Creating the custom login page

In our example, we'll create a login page, named "myLoginPage.jsp"  that is actually quite similar to the one that Spring Security generates   except that we'll add a logout button:

<%@ taglib prefix="s" uri="http://www.springframework.org/tags"%>
<%@ taglib prefix="security"
 uri="http://www.springframework.org/security/tags"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<%@ taglib prefix="sf" uri="http://www.springframework.org/tags/form"%>
<div id="loginPannel">
 <security:authorize access="!isAuthenticated()">
  <h1>This is my custom login page</h1>
  
  <c:if test="${loginFailed}">
   <div style="color: red">Could not sign in, please check your login/password...</div>
  </c:if>  

  <form method="post" class="signin" action="j_spring_security_check">
   <table>
    <tr>
     <th><label for="username_or_email">Username/email</label></th>
     <td><input id="username_or_email" name="j_username" type="text" /></td>
    </tr>
    <tr>
     <th><label for="password">Password</label></th>
     <td><input id="password" name="j_password" type="password" /></td>
    </tr>
    <tr>
     <th></th>
     <td><input name="commit" type="submit" value="Sign In" /></td>
    </tr>
   </table>
  </form>
 </security:authorize>
</div>

Note the following line in our JSP page: <security:authorize access="!isAuthenticated()">
It specifies that this page is restricted to user that aren't authenticated. Access rules can be defined using expressions which correspond to the methods (such as, hasRole( ), hasAnyRole( ), ...) from the "SecurityExpressionRoot" provided by Spring.

STEP 2 - Customizing login page

In this step, we edit the Spring configuration file "MyDispatcherServlet-servlet.xml", to override some behavior of Spring default security configuration:

 <security:http auto-config="true" use-expressions="true"> 
  <security:form-login login-page="/login.go" default-target-url="/home.go" authentication-failure-url="/login.go?errorLogin"/> 
  <security:intercept-url pattern="/home.go" access="hasRole('ADMIN')" /> 
  <security:logout logout-success-url="/home.go" />
 </security:http>


  • use-expression="true": allows us to use expression such as hasRole(...), hasAnyRole(...), isAuthenticated( ), and so on, to define access rules
  • login-page="/login.go": defines the URL at which the login page is located
  • default-target-url="/home.go": defines the URL toward which the user will be redirected once he successfully logged in
  • authentication-failure-url="/login.go?errorLogin": defines the URL toward which the user will be redirected when the login fails. In our example, we redirect the user to the login page with an additional "errorLogin" parameter in the URL. This existence of this parameter will be checked in a Spring Controller class. If it exists, an attribute will be added to the model before returning the view to be displayed. Within the JSP page, we check this model attribute to eventually display an error message. 
  • <security:intercept-url pattern="/home.go" access="hasRole('ADMIN')" />: indicates that the "/home.go" URL is subject to access rules.
  • <security:logout logout-success-url="/home.go" />: defines the URL at which the user is redirected after he logged out.
STEP 3 - Defining a custom authentication provider

In most cases, we'll store user credentials and roles in a DB, LDAP, you name it, and consequently, we'll want authentication and authorization to be performed against that user repository. In this example, we'll configure an authentication provider as if we were storing the user credentials in a DB.

To do so, we edit the Spring configuration file "MyDispatcherServlet-servlet.xml" and define a custom Spring service (here, it's named "myUserDetailService") as authentication provider:

 <security:authentication-manager>
  <security:authentication-provider
   user-service-ref="myUserDetailService">
  </security:authentication-provider>
 </security:authentication-manager>

STEP4 - Implementing the custom authentication provider


  • First, we have to create a class that implements the GrantedAuthority interface from Spring. This interface will have us implement a getAuthority( ) method that returns a role name.
Ex:

package service.security;

import org.springframework.security.core.GrantedAuthority;

public class GrantedAuthorityImpl implements GrantedAuthority{
 private static final long serialVersionUID = 1029928088340565343L;

 private String rolename;
 
 public GrantedAuthorityImpl(String rolename){
  this.rolename = rolename;
 }
 
 public String getAuthority() {
  return this.rolename;
 }

}


  • Then, we create a class that implements the UserDetails interface from Spring. This class will hold all the account-related info such as the user credentials and the associated roles.
package service.security;

import java.util.Collection;
import java.util.HashSet;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

public class UserDetailsImpl implements UserDetails{
 private static final long serialVersionUID = -6509897037222767090L;
 
 private Collection authorities = new HashSet();
 private String password;
 private String username;
 
 public UserDetailsImpl(String username, String password, Collection authorities){
  this.username = username; 
  this.password = password;
  this.authorities = authorities;
 }

 public Collection getAuthorities() {
  return this.authorities;
 }

 public String getPassword() {
  return this.password;
 }

 public String getUsername() {
  return this.username;
 }

 public boolean isAccountNonExpired() {
  return true;
 }

 public boolean isAccountNonLocked() {
  return true;
 }

 public boolean isCredentialsNonExpired() {
  return true;
 }

 public boolean isEnabled() {
  return true;
 }

}

  • Finally, we create a class that implements the UserDetailsService interface from Spring. This interface contains only one method: UserDetails loadUserByUsername(String username). In our example, we emulate a DB user repository by defining a Map whose keys are usernames, and values are UserDetails objects. Pay attention to the static initialization block in which we populate this map with 3 users.
Remark:

If we actually had user credentials stored in DB, we would just have to implement a repository class, and inject it into this class.
package service.security;

import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

import org.springframework.security.core.GrantedAuthority;
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;

@Service("myUserDetailService")
public class UserDetailsServiceImpl implements UserDetailsService{
 
 // just to emulate user data and credentials retrieval from a DB, or whatsoever authentication service
 private static Map<String, UserDetails> userRepository = new HashMap<String, UserDetails>();
 
 static{
  GrantedAuthority authorityAdmin = new GrantedAuthorityImpl("ADMIN");
  GrantedAuthority authorityGuest = new GrantedAuthorityImpl("GUEST");
  
  /* user1/password1 --> ADMIN */
  Set<GrantedAuthority> authorities1 = new HashSet<GrantedAuthority>();
  authorities1.add(authorityAdmin);
  UserDetails user1 = new UserDetailsImpl("user1", "password1", authorities1);
  userRepository.put("user1", user1);
  
  /* user2/password2 --> GUEST */
  Set<GrantedAuthority> authorities2 = new HashSet<GrantedAuthority>();
  authorities2.add(authorityGuest);
  UserDetails user2 = new UserDetailsImpl("user2", "password2", authorities2);
  userRepository.put("user2", user2);
  
  /* user3/password3 --> ADMIN + GUEST */
  Set<GrantedAuthority> authorities3 = new HashSet<GrantedAuthority>();
  authorities3.add(authorityAdmin);
  authorities3.add(authorityGuest);
  UserDetails user3 = new UserDetailsImpl("user3", "password3", authorities3);
  userRepository.put("user3", user3);
 }
 
 public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
  UserDetails matchingUser = userRepository.get(username);
  
  if(matchingUser == null){
   throw new UsernameNotFoundException("Wrong username or password");
  }
  
  return matchingUser;
 }

}


  • In the Controller class, we add the two following URL mapping. As as result, GET requests for /login URL will display the custom login page, while requests for the same URL with the "errorLogin" parameter will display the same custom login page with an additional "loginFailed" model attribute. This attribute is checked in the JSP page, to display an error message, if neccessary.
 @RequestMapping(method=RequestMethod.GET, value="/login")
 public String displayLoginPage(){
  return "myLoginPage";
 }
 
 @RequestMapping(value="/login", params="errorLogin")
 public String directToLoginPageWithError(Model model){
  // Adding an attribute to flag that an error happened at login
  model.addAttribute("loginFailed", true);

  return "myLoginPage";
 }

Testing the application

  • Custom login page:
  • Login with bad credentials


  • Login with user1/password1
  • Login with user2/password2

In this last case, user2 is authenticated but not authorized to access the home.go page. In the Spring configuration "MyDispatcherServlet-servlet.xml" file, we specified that the home.go page is restricted to user with role ADMIN with the following configuration element:

<security:intercept-url pattern="/home.go" access="hasRole('ADMIN')" /> 

Source code

The source code of this example application can be checked out here.

No comments:

Post a Comment

Note: Only a member of this blog may post a comment.