Sunday, January 30, 2011

Spring Security 3: Full ACL Tutorial (Part 3)

In Part 1 of this tutorial we've completed setting up the ACL and Bulletin databases. In Part 2 we've completed the Spring Security configuration. In Part 3 we'll be developing the Spring MVC module of the application.

Part 1: Functional Specs and the Application Database
Part 2: Spring Security Configuration
Part 3: Spring MVC Module
Part 4: Running the Application

Part 3: Spring MVC

The Spring MVC module development is quite straightforward. We'll be creating the following:
1. domain objects
2. services
3. controllers
4. configuration files

The Domain Objects

In Part 1 we discussed in the Functional Specs section that our Bulletin application has three types of posts:
AdminPost - contains an id, date, and message
PersonalPost - contains an id, date, and message
PublicPost - contains an id, date, and message
These posts correspond to the domain objects of the system. Let's start by creating a common interface:

Post.java
package org.krams.tutorial.domain;

import java.util.Date;

/**
 * A simple interface for Post objects
 */
public interface Post {

 public Long getId();

 public void setId(Long id);

 public Date getDate();

 public void setDate(Date date);

 public String getMessage();

 public void setMessage(String message);

}
The method signatures correspond to the properties we've discussed in the Functional Specs section.

Let's now create three concrete classes:

AdminPost.java
package org.krams.tutorial.domain;

import java.util.Date;

/**
 * A simple POJO representing admin posts
 */
public class AdminPost implements Post {
 private Long id;
 private Date date;
 private String message;
 
 public Long getId() {
  return id;
 }

 public void setId(Long id) {
  this.id = id;
 }

 public Date getDate() {
  return date;
 }

 public void setDate(Date date) {
  this.date = date;
 }

 public String getMessage() {
  return message;
 }

 public void setMessage(String message) {
  this.message = message;
 }
}

PersonalPost.java
package org.krams.tutorial.domain;

import java.util.Date;

/**
 * A simple POJO representing personal posts
 */
public class PersonalPost implements Post {
 private Long id;
 private Date date;
 private String message;
 
 public Long getId() {
  return id;
 }
 
 public void setId(Long id) {
  this.id = id;
 }
 
 public Date getDate() {
  return date;
 }
 
 public void setDate(Date date) {
  this.date = date;
 }
 
 public String getMessage() {
  return message;
 }
 
 public void setMessage(String message) {
  this.message = message;
 }
}

PublicPost.java
package org.krams.tutorial.domain;

import java.util.Date;

/**
 * A simple POJO representing public posts
 */
public class PublicPost implements Post {
 private Long id;
 private Date date;
 private String message;
 
 public Long getId() {
  return id;
 }
 
 public void setId(Long id) {
  this.id = id;
 }
 
 public Date getDate() {
  return date;
 }
 
 public void setDate(Date date) {
  this.date = date;
 }
 
 public String getMessage() {
  return message;
 }
 
 public void setMessage(String message) {
  this.message = message;
 }
}
Our concrete classes are just simple POJOs.

The Services

We'll be declaring three services where each one will handle a specific domain object:
AdminService - handles AdminPost
PersonalService - handles PersonalPost
PublicService - handles PublicPost

Our services will implement a common interface. It's worth noting this interface is the crux of successfully applying ACL in the application. Therefore, we've heavily placed comments within this interface to point out important information.

Here's the interface:

GenericService.java
package org.krams.tutorial.service;

import java.util.List;

import javax.sql.DataSource;

import org.krams.tutorial.domain.Post;
import org.springframework.security.access.prepost.PostAuthorize;
import org.springframework.security.access.prepost.PostFilter;
import org.springframework.security.access.prepost.PreAuthorize;

/**
 * A generic service for handling CRUD operations.
 * <p>
 * The method access-control expressions are specified in this interface.
 */
public interface GenericService {

 /**
  * Inject the datasource for the bulletingapplication
  */
 public void setDataSource(DataSource dataSource);

 /**
  *  Retrieves a single post.
  *  <p>
  *  Access-control will be evaluated after this method is invoked.
  *  returnObject refers to the returned object.
  */
 @PostAuthorize("hasPermission(returnObject, 'WRITE')")
 public Post getSingle(Long id);

 /**
  *  Retrieves all posts.
  *  <p>
  *  Access-control will be evaluated after this method is invoked.
  *  filterObject refers to the returned object list.
  */
 @PostFilter("hasPermission(filterObject, 'READ')")
 public List<Post> getAll();

 /**
  * Adds a new post.
  * <p>
  * We don't provide any access control here because  
  * the new object doesn't have an id yet. 
  * <p>
  * Instead we place the access control on the URL-level because
  * the Add page shouldn't be visible in the first place.
  * <p>
  * There are two places where we can place this restriction:
  * <pre>
  * 1. At the controller method
  * 2. At the external spring-security.xml file</pre>
  * <p>
  * 
  */
 public Boolean add(Post post);

 /**
  * Edits a post.
  * <p>
  * Access-control will be evaluated before this method is invoked.
  * <b>#post</b> refers to the current object in the method argument. 
  */
 @PreAuthorize("hasPermission(#post, 'WRITE')")
 public Boolean edit(Post post);

 /**
  * Deletes a post.
  * <p>
  * Access-control will be evaluated before this method is invoked.
  * <b>#post</b> refers to the current object in the method argument. 
  */
 @PreAuthorize("hasPermission(#post, 'WRITE')")
 public Boolean delete(Post post);

}
Notice the methods had been annotated with different types of Expression-based access controls, and because this is an interface all concrete classes will be secured with ACL.

Method Access-Control Expressions

Let's take an in-depth look at all the important annotations:

@PostAuthorize
@PostAuthorize("hasPermission(returnObject, 'WRITE')")
public Post getSingle(Long id);
Annotation for specifying a method access-control expression which will be evaluated after a method has been invoked - Source: Spring Security 3 API
This annotation will be triggered after the getSingle() method has completed its execution. The rule that will be applied is inside the annotation:
"hasPermission(returnObject, 'WRITE')"
This means whatever is the returned object (in our case, a Post object) make sure the current user has a WRITE access.

@PostFilter
@PostFilter("hasPermission(filterObject, 'READ')")
public List getAll();
Annotation for specifying a method filtering expression which will be evaluated after a method has been invoked - Source: Spring Security 3 API
This annotation will be triggered after the getAll() method has completed its execution. The rule that will be applied is inside the annotation:
"hasPermission(filterObject, 'READ')"
This means whatever is the returned list of objects (in our case, Post objects) make sure to filter the list. Return only objects where the current user has READ access.


@PreAuthorize("hasPermission(#post, 'WRITE')")
public Boolean edit(Post post);
Annotation for specifying a method access-control expression which will be evaluated to decide whether a method invocation is allowed or not - Source: Spring Security 3 API
This annotation will be triggered before the edit() method is executed. The rule that will be applied is inside the annotation:
"hasPermission(#post, 'WRITE')"
This means whatever is the object argument (in our case, a Post object) make sure the current user has WRITE access.

Extra Pointers

Notice all of the methods are annotated with method access-control expressions, except for two:
1. public void setDataSource(DataSource dataSource);
2. public Boolean add(Post post);
The method setDataSource() has nothing to do with Spring Security. It's just a reference to the application's datasource, the bulletinDataSource, which we'll discuss later.

The method add() is what's interesting. Remember the Functional Specs in Part 1:
1. Only users with ROLE_ADMIN can create AdminPost
2. Only users with ROLE_USER can create PersonalPost
3. Only users with ROLE_ADMIN or ROLE_USER can create PublicPost
4. Users with ROLE_VISITOR cannot create any post
Note: When we use the word 'create', we mean adding a new post.

Post creation is one of the crucial methods available in the application, but it's left unsecured! Here are the reasons why:

The object reference inside the hasPermission expression needs to have an existing id. But when creating a new Post, it doesn't have an id yet from the bulletin database!

Solution #1 (Bad)
We could use the @PostAuthorize expression to allow the user to add. Then check the returned object's id for valid permission. But this expression defeats the main purpose. The user has already created a record regardless of permission!

Solution #2 (Bad)
We could use the @PreAuthorize expression to verify if the current user has valid permissions before executing the add() method. But wait. That's wrong! The Post object doesn't have an existing id yet because it hasn't been created in the database!

There's another question: why does the object need an existing id? The simple answer is because the default PermissionEvaluator implementation and the default Acl implementation requires one. If we need to bypass this limitation, we can create our own implementations.

Solution #3 (Good)
Apply access control at the URL-level. In the first place, we don't really want to show the add new post page. Then it's better if we limit control at the URL-level. There are two ways where we can implement this:
a. add an intercept-url in the spring-security.xml 
b. apply method security control expression in the controller 
We'll choose option b because it's an interesting solution and allows us to explore method security control expressions at the controller level.

The Controllers

Our application controllers share the same implementation where the main difference only are the @RequestMapping values and the services.

We'll be declaring four controllers:
AdminController - handles admin related requests
PersonalController - handles user related requests
PublicController - handles visitor related requests
BulletinController - handles the general view all requests

AdminController.java
package org.krams.tutorial.controller;

import java.util.Date;

import org.apache.log4j.Logger;
import org.krams.tutorial.domain.AdminPost;
import org.krams.tutorial.domain.Post;
import org.krams.tutorial.service.GenericService;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;

import javax.annotation.Resource;

/**
 * Handles Admin-related requests
 */
@Controller
@RequestMapping("/admin")
public class AdminController {

 protected static Logger logger = Logger.getLogger("controller");
 
 @Resource(name="adminService")
 private GenericService adminService;

 /**
     * Retrieves the Edit page
     */
    @RequestMapping(value = "/edit", method = RequestMethod.GET)
    public String getEdit(@RequestParam(value="id", required=true) Long id,  
              Model model) {
     logger.debug("Received request to show edit page");
    
     // Retrieve existing post and add to model
     // This is the formBackingOBject
     model.addAttribute("postAttribute", adminService.getSingle(id));
     
     // Add source to model to help us determine the source of the JSP page
     model.addAttribute("source", "Admin");
     
     // This will resolve to /WEB-INF/jsp/crud-admin/editpage.jsp
     return "crud-admin/editpage";
 }
    
    /**
     * Saves the edited post from the Edit page and returns a result page.
     */
    @RequestMapping(value = "/edit", method = RequestMethod.POST)
    public String getEditPage(@ModelAttribute("postAttribute") AdminPost post, 
              @RequestParam(value="id", required=true) Long id,
              Model model) {
     logger.debug("Received request to view edit page");
    
     // Re-assign id
     post.setId(id);
     // Assign new date
     post.setDate(new Date());
     
     // Delegate to service
     if (adminService.edit(post) == true) {
         // Add result to model
         model.addAttribute("result", "Entry has been edited successfully!");
     } else {
         // Add result to model
         model.addAttribute("result", "You're not allowed to perform that action!");
     }

     // Add source to model to help us determine the source of the JSP page
     model.addAttribute("source", "Admin");
     
     // Add our current role and username
     model.addAttribute("role", SecurityContextHolder.getContext().getAuthentication().getAuthorities());
     model.addAttribute("username", SecurityContextHolder.getContext().getAuthentication().getName());
     
     // This will resolve to /WEB-INF/jsp/crud-admin/resultpage.jsp
     return "crud-admin/resultpage";
 }
    
    /**
     * Retrieves the Add page
     * <p>
     * Access-control is placed here (instead in the service) because we don't want 
     * to show this page if the client is unauthorized and because the new 
     * object doesn't have an id. The hasPermission requires an existing id!
     */
 @PreAuthorize("hasAuthority('ROLE_ADMIN')")
    @RequestMapping(value = "/add", method = RequestMethod.GET)
    public String getAdd(Model model) {
     logger.debug("Received request to show add page");
    
     // Create new post and add to model
     // This is the formBackingOBject
     model.addAttribute("postAttribute", new AdminPost());

     // Add source to model to help us determine the source of the JSP page
     model.addAttribute("source", "Admin");
     
     // This will resolve to /WEB-INF/jsp/crud-admin/addpage.jsp
     return "crud-admin/addpage";
 }
    
    /**
     * Saves a new post from the Add page and returns a result page.
     */
    @RequestMapping(value = "/add", method = RequestMethod.POST)
    public String getAddPage(@ModelAttribute("postAttribute") AdminPost post, Model model) {
     logger.debug("Received request to view add page");
    
     // Add date today
     post.setDate(new Date());
     
     // Delegate to service
     if (adminService.add(post)) {
         // Success. Add result to model
         model.addAttribute("result", "Entry has been added successfully!");
     } else {
         // Failure. Add result to model
         model.addAttribute("result", "You're not allowed to perform that action!");
     }
     
     // Add source to model to help us determine the source of the JSP page
     model.addAttribute("source", "Admin");
     
     // Add our current role and username
     model.addAttribute("role", SecurityContextHolder.getContext().getAuthentication().getAuthorities());
     model.addAttribute("username", SecurityContextHolder.getContext().getAuthentication().getName());
     
     // This will resolve to /WEB-INF/jsp/crud-admin/resultpage.jsp
     return "crud-admin/resultpage";
 }
    
    /**
     * Deletes an existing post and returns a result page.
     */
    @RequestMapping(value = "/delete", method = RequestMethod.GET)
    public String getDeletePage(@RequestParam(value="id", required=true) Long id,
              Model model) {
     logger.debug("Received request to view delete page");
    
     // Create new post
     Post post = new AdminPost();
     // Assign id
     post.setId(id);
     
     // Delegate to service
     if (adminService.delete(post)) {
         // Add result to model
         model.addAttribute("result", "Entry has been deleted successfully!");
     } else {
         // Add result to model
         model.addAttribute("result", "You're not allowed to perform that action!");
     }
     
     // Add source to model to help us determine the source of the JSP page
     model.addAttribute("source", "Admin");
     
     // Add our current role and username
     model.addAttribute("role", SecurityContextHolder.getContext().getAuthentication().getAuthorities());
     model.addAttribute("username", SecurityContextHolder.getContext().getAuthentication().getName());
     
     // This will resolve to /WEB-INF/jsp/crud-admin/resultpage.jsp
     return "crud-admin/resultpage";
 }
}

PersonalController.java
package org.krams.tutorial.controller;

import java.util.Date;

import org.apache.log4j.Logger;
import org.krams.tutorial.domain.PersonalPost;
import org.krams.tutorial.domain.Post;
import org.krams.tutorial.service.GenericService;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;

import javax.annotation.Resource;

/**
 * Handles Personal-related requests
 */
@Controller
@RequestMapping("/personal")
public class PersonalController {

 protected static Logger logger = Logger.getLogger("controller");
 
 @Resource(name="personalService")
 private GenericService personalService;
    
 /**
     * Retrieves the Edit page
     */
    @RequestMapping(value = "/edit", method = RequestMethod.GET)
    public String getEdit(@RequestParam(value="id", required=true) Long id,  
              Model model) {
     logger.debug("Received request to show edit page");
    
     // Retrieve existing post and add to model
     // This is the formBackingOBject
     model.addAttribute("postAttribute", personalService.getSingle(id));
     
     // Add source to model to help us determine the source of the JSP page
     model.addAttribute("source", "Personal");
     
     // This will resolve to /WEB-INF/jsp/crud-personal/editpage.jsp
     return "crud-personal/editpage";
 }
    
    /**
     * Saves the edited post from the Edit page and returns a result page.
     */
    @RequestMapping(value = "/edit", method = RequestMethod.POST)
    public String getEditPage(@ModelAttribute("postAttribute") PersonalPost post, 
              @RequestParam(value="id", required=true) Long id,
              Model model) {
     logger.debug("Received request to view edit page");
    
     // Re-assign id
     post.setId(id);
     // Assign new date
     post.setDate(new Date());
     
     // Delegate to service
     if (personalService.edit(post) == true) {
         // Add result to model
         model.addAttribute("result", "Entry has been edited successfully!");
     } else {
         // Add result to model
         model.addAttribute("result", "You're not allowed to perform that action!");
     }

     // Add source to model to help us determine the source of the JSP page
     model.addAttribute("source", "Personal");
     
     // Add our current role and username
     model.addAttribute("role", SecurityContextHolder.getContext().getAuthentication().getAuthorities());
     model.addAttribute("username", SecurityContextHolder.getContext().getAuthentication().getName());
     
     // This will resolve to /WEB-INF/jsp/crud-personal/resultpage.jsp
     return "crud-personal/resultpage";
 }
    
    /**
     * Retrieves the Add page
     * <p>
     * Access-control is placed here (instead in the service) because we don't want 
     * to show this page if the client is unauthorized and because the new 
     * object doesn't have an id. The hasPermission requires an existing id!
     */
 @PreAuthorize("hasAuthority('ROLE_USER') and !hasAuthority('ROLE_ADMIN')")
    @RequestMapping(value = "/add", method = RequestMethod.GET)
    public String getAdd(Model model) {
     logger.debug("Received request to show add page");
    
     // Create new post and add to model
     // This is the formBackingOBject
     model.addAttribute("postAttribute", new PersonalPost());

     // Add source to model to help us determine the source of the JSP page
     model.addAttribute("source", "Personal");
     
     // This will resolve to /WEB-INF/jsp/crud-personal/addpage.jsp
     return "crud-personal/addpage";
 }
    
    /**
     * Saves a new post from the Add page and returns a result page.
     */
    @RequestMapping(value = "/add", method = RequestMethod.POST)
    public String getAddPage(@ModelAttribute("postAttribute") PersonalPost post, Model model) {
     logger.debug("Received request to view add page");
    
     // Add date today
     post.setDate(new Date());
     
     // Delegate to service
     if (personalService.add(post)) {
         // Success. Add result to model
         model.addAttribute("result", "Entry has been added successfully!");
     } else {
         // Failure. Add result to model
         model.addAttribute("result", "You're not allowed to perform that action!");
     }
     
     // Add source to model to help us determine the source of the JSP page
     model.addAttribute("source", "Personal");
     
     // Add our current role and username
     model.addAttribute("role", SecurityContextHolder.getContext().getAuthentication().getAuthorities());
     model.addAttribute("username", SecurityContextHolder.getContext().getAuthentication().getName());
     
     // This will resolve to /WEB-INF/jsp/crud-personal/resultpage.jsp
     return "crud-personal/resultpage";
 }
    
    /**
     * Deletes an existing post and returns a result page.
     */
    @RequestMapping(value = "/delete", method = RequestMethod.GET)
    public String getDeletePage(@RequestParam(value="id", required=true) Long id,
              Model model) {
     logger.debug("Received request to view delete page");
    
     // Create new post
     Post post = new PersonalPost();
     // Assign id
     post.setId(id);
     
     // Delegate to service
     if (personalService.delete(post)) {
         // Add result to model
         model.addAttribute("result", "Entry has been deleted successfully!");
     } else {
         // Add result to model
         model.addAttribute("result", "You're not allowed to perform that action!");
     }
     
     // Add source to model to help us determine the source of the JSP page
     model.addAttribute("source", "Personal");
     
     // Add our current role and username
     model.addAttribute("role", SecurityContextHolder.getContext().getAuthentication().getAuthorities());
     model.addAttribute("username", SecurityContextHolder.getContext().getAuthentication().getName());
     
     // This will resolve to /WEB-INF/jsp/crud-personal/resultpage.jsp
     return "crud-personal/resultpage";
 }
}

PublicController.java
package org.krams.tutorial.controller;

import java.util.Date;

import org.apache.log4j.Logger;
import org.krams.tutorial.domain.PublicPost;
import org.krams.tutorial.domain.Post;
import org.krams.tutorial.service.GenericService;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;

import javax.annotation.Resource;

/**
 * Handles Public-related requests
 */
@Controller
@RequestMapping("/public")
public class PublicController {

 protected static Logger logger = Logger.getLogger("controller");
 
 @Resource(name="publicService")
 private GenericService publicService;
    
 /**
     * Retrieves the Edit page
     */
    @RequestMapping(value = "/edit", method = RequestMethod.GET)
    public String getEdit(@RequestParam(value="id", required=true) Long id,  
              Model model) {
     logger.debug("Received request to show edit page");
    
     // Retrieve existing post and add to model
     // This is the formBackingOBject
     model.addAttribute("postAttribute", publicService.getSingle(id));
     
     // Add source to model to help us determine the source of the JSP page
     model.addAttribute("source", "Public");
     
     // This will resolve to /WEB-INF/jsp/crud-public/editpage.jsp
     return "crud-public/editpage";
 }
    
    /**
     * Saves the edited post from the Edit page and returns a result page.
     */
    @RequestMapping(value = "/edit", method = RequestMethod.POST)
    public String getEditPage(@ModelAttribute("postAttribute") PublicPost post, 
              @RequestParam(value="id", required=true) Long id,
              Model model) {
     logger.debug("Received request to view edit page");
    
     // Re-assign id
     post.setId(id);
     // Assign new date
     post.setDate(new Date());
     
     // Delegate to service
     if (publicService.edit(post) == true) {
         // Add result to model
         model.addAttribute("result", "Entry has been edited successfully!");
     } else {
         // Add result to model
         model.addAttribute("result", "You're not allowed to perform that action!");
     }

     // Add source to model to help us determine the source of the JSP page
     model.addAttribute("source", "Public");
     
     // Add our current role and username
     model.addAttribute("role", SecurityContextHolder.getContext().getAuthentication().getAuthorities());
     model.addAttribute("username", SecurityContextHolder.getContext().getAuthentication().getName());
     
     // This will resolve to /WEB-INF/jsp/crud-public/resultpage.jsp
     return "crud-public/resultpage";
 }
    
    /**
     * Retrieves the Add page
     * <p>
     * Access-control is placed here (instead in the service) because we don't want 
     * to show this page if the client is unauthorized and because the new 
     * object doesn't have an id. The hasPermission requires an existing id!
     */
 @PreAuthorize("hasAuthority('ROLE_USER') or hasAuthority('ROLE_ADMIN')")
    @RequestMapping(value = "/add", method = RequestMethod.GET)
    public String getAdd(Model model) {
     logger.debug("Received request to show add page");
    
     // Create new post and add to model
     // This is the formBackingOBject
     model.addAttribute("postAttribute", new PublicPost());

     // Add source to model to help us determine the source of the JSP page
     model.addAttribute("source", "Public");
     
     // This will resolve to /WEB-INF/jsp/crud-public/addpage.jsp
     return "crud-public/addpage";
 }
    
    /**
     * Saves a new post from the Add page and returns a result page.
     */
    @RequestMapping(value = "/add", method = RequestMethod.POST)
    public String getAddPage(@ModelAttribute("postAttribute") PublicPost post, Model model) {
     logger.debug("Received request to view add page");
    
     // Add date today
     post.setDate(new Date());
     
     // Delegate to service
     if (publicService.add(post)) {
         // Success. Add result to model
         model.addAttribute("result", "Entry has been added successfully!");
     } else {
         // Failure. Add result to model
         model.addAttribute("result", "You're not allowed to perform that action!");
     }
     
     // Add source to model to help us determine the source of the JSP page
     model.addAttribute("source", "Public");
     
     // Add our current role and username
     model.addAttribute("role", SecurityContextHolder.getContext().getAuthentication().getAuthorities());
     model.addAttribute("username", SecurityContextHolder.getContext().getAuthentication().getName());
     
     // This will resolve to /WEB-INF/jsp/crud-public/resultpage.jsp
     return "crud-public/resultpage";
 }
    
    /**
     * Deletes an existing post and returns a result page.
     */
    @RequestMapping(value = "/delete", method = RequestMethod.GET)
    public String getDeletePage(@RequestParam(value="id", required=true) Long id,
              Model model) {
     logger.debug("Received request to view delete page");
    
     // Create new post
     Post post = new PublicPost();
     // Assign id
     post.setId(id);
     
     // Delegate to service
     if (publicService.delete(post)) {
         // Add result to model
         model.addAttribute("result", "Entry has been deleted successfully!");
     } else {
         // Add result to model
         model.addAttribute("result", "You're not allowed to perform that action!");
     }
     
     // Add source to model to help us determine the source of the JSP page
     model.addAttribute("source", "Public");
     
     // Add our current role and username
     model.addAttribute("role", SecurityContextHolder.getContext().getAuthentication().getAuthorities());
     model.addAttribute("username", SecurityContextHolder.getContext().getAuthentication().getName());
     
     // This will resolve to /WEB-INF/jsp/crud-public/resultpage.jsp
     return "crud-public/resultpage";
 }
}

BulletinController.java
/**
 * 
 */
package org.krams.tutorial.controller;

import javax.annotation.Resource;

import org.apache.log4j.Logger;
import org.krams.tutorial.service.GenericService;
import org.krams.tutorial.service.PersonalService;
import org.krams.tutorial.service.PublicService;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

/**
 * Handles Bulletin related requests
 */
@Controller
@RequestMapping("/bulletin")
public class BulletinController{
        
 protected static Logger logger = Logger.getLogger("controller");

 @Resource(name="adminService")
 private GenericService adminService;
 
 @Resource(name="personalService")
 private GenericService personalService;
 
 @Resource(name="publicService")
 private GenericService publicService;
 
 /**
  * Retrieves the View page. 
  * <p>
  * This loads all authorized posts.
  */
    @RequestMapping(value = "/view", method = RequestMethod.GET)
    public String getViewAllPage(Model model) {
     logger.debug("Received request to view all page");
    
     // Retrieve items from service and add to model
     model.addAttribute("adminposts", adminService.getAll());
     model.addAttribute("personalposts", personalService.getAll());
     model.addAttribute("publicposts", publicService.getAll());
     
     // Add our current role and username
     model.addAttribute("role", SecurityContextHolder.getContext().getAuthentication().getAuthorities());
     model.addAttribute("username", SecurityContextHolder.getContext().getAuthentication().getName());
     
     // This will resolve to /WEB-INF/jsp/bulletinpage.jsp
     return "bulletinpage";
 }
}

Spring MVC Configuration

We've completed the Spring MVC classes of our application. Our next task is to declare the required configuration.

web.xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app id="WebApp_ID" version="2.4" xmlns="http://java.sun.com/xml/ns/j2ee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd">

 <filter>
         <filter-name>springSecurityFilterChain</filter-name>
         <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
 </filter>
 
 <filter-mapping>
         <filter-name>springSecurityFilterChain</filter-name>
         <url-pattern>/*</url-pattern>
 </filter-mapping>

 <context-param>
  <param-name>contextConfigLocation</param-name>
  <param-value>
  /WEB-INF/spring-security.xml
  /WEB-INF/applicationContext.xml
  </param-value>
 </context-param>
 
 <servlet>
  <servlet-name>spring</servlet-name>
  <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
  <load-on-startup>1</load-on-startup>
 </servlet>
 
 <servlet-mapping>
  <servlet-name>spring</servlet-name>
  <url-pattern>/krams/*</url-pattern>
 </servlet-mapping>

 <listener>
  <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
 </listener>
 
</web-app>

Take note of the URL pattern. When accessing any pages in our MVC application, the host name must be appended with
/krams
In the web.xml we declared a servlet-name spring. By convention, we must declare a spring-servlet.xml as well.

spring-servlet.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 xmlns:p="http://www.springframework.org/schema/p" 
 xsi:schemaLocation="http://www.springframework.org/schema/beans 
      http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">
 
 <!-- Declare a view resolver -->
 <bean id="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver" 
      p:prefix="/WEB-INF/jsp/" p:suffix=".jsp" />

</beans>
This XML config declares a view resolver. All references to a JSP name in the controllers will map to a corresponding JSP in the /WEB-INF/jsp location.

By convention, we must declare an applicationContext.xml

applicationContext.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
 xmlns:context="http://www.springframework.org/schema/context"
 xmlns:mvc="http://www.springframework.org/schema/mvc"
 xsi:schemaLocation="http://www.springframework.org/schema/beans 
      http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
      http://www.springframework.org/schema/context
      http://www.springframework.org/schema/context/spring-context-3.0.xsd
   http://www.springframework.org/schema/mvc 
   http://www.springframework.org/schema/mvc/spring-mvc-3.0.xsd">
 
 <!-- Activates various annotations to be detected in bean classes -->
 <context:annotation-config />
 
 <!-- Scans the classpath for annotated components that will be auto-registered as Spring beans.
  For example @Controller and @Service. Make sure to set the correct base-package-->
 <context:component-scan base-package="org.krams.tutorial" />
 
 <!-- Configures the annotation-driven Spring MVC Controller programming model.
 Note that, with Spring 3.0, this tag works in Servlet MVC only!  -->
 <mvc:annotation-driven /> 
 
 <!-- Loads bulletin related configuration-->
 <import resource="bulletin-context.xml" />
</beans>

Conclusion

We've completed the Spring MVC module of the Bulletin application. We've declared the required domain objects, services, and controllers. Our final task is to test and run the application. We'll also cover some of the unexpected problems within the application.

Proceed to Part 4: Running the Application
StumpleUpon DiggIt! Del.icio.us Blinklist Yahoo Furl Technorati Simpy Spurl Reddit Google I'm reading: Spring Security 3: Full ACL Tutorial (Part 3) ~ Twitter FaceBook

Subscribe by reader Subscribe by email Share

7 comments:

  1. Pfeeeeeeeeee !! it was long but it's a very very very GOOD TUTORIAL ! Thx man for it ! (where is the code source ? :-( )

    ReplyDelete
  2. @Anonymous, thanks for the comment. I think you forgot to look at part 4 :-) The source code is there.

    ReplyDelete
  3. Excellent! but why you used @PreAuthorize("hasAuthority('ROLE_ADMIN')") in admin controller this method is already secured using URL level security

    ReplyDelete
  4. thanks for all these great tutorials

    ReplyDelete
  5. These tutorials are holding up great. Thanks for the help. Just a quick note the object-reference in the @PreAuthorize annotation no longer works in interfaces. So this no longer works: "@PreAuthorize("hasPermission(#post, 'WRITE')")" You would have to annotate the method parameter with @Param("post"). Like this:

    @PreAuthorize("hasPermission(#post, 'WRITE')")
    public Boolean edit(@Param("post") Post post);

    ReplyDelete
  6. I have read your blog its very attractive and impressive. I like it your blog.

    Spring online training Spring online training Spring Hibernate online training Spring Hibernate online training Java online training

    spring training in chennai spring hibernate training in chennai

    ReplyDelete