Requirements:
- a working example of JSF 2.0 application with Spring Framework integrated (can be found here)
You will learn:
- how to use Spring Security Framework in order to protect web application
What parts of our application will be protected? Consider those scenarios:
1. Only registered user (or page administrator) can see details of a selected bike.
2. Only page administrator can add a new bike to the shop offer.
We need two user roles which will determine the privilleges which user has: registered users role and admin users role. Moreover, for the scenario 2, we have to add a new function: adding new bike. For this function we will create a page addBike.xhtml and a JSF managed bean for that page, named addBike.java.
addBike.xhtml source code is shown below:
<?xml version='1.0' encoding='UTF-8' ?>Nothing special - standard form for entering the data.
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core">
<ui:composition template="../shopTemplate.xhtml">
<ui:define name="content">
<h:form>
<h:outputText value="#{msg['bikes.list.name']}: "/><h:inputText value="#{addBike.name}" /><br/>
<h:outputText value="#{msg['bikes.list.price']}: "/><h:inputText value="#{addBike.price}" /><br/>
<h:outputText value="#{msg['bikes.list.discountprice']}: "/><h:inputText value="#{addBike.discountPrice}" /><br/>
<h:outputText value="#{msg['bikes.list.description']}: "/><h:inputText value="#{addBike.description}" /><br/>
<h:commandButton action="#{addBike.addNewBike}" value="#{msg['bikes.add.button']}" />
</h:form>
</ui:define>
</ui:composition>
</html>
addBike.java source code is also simple:
package com.jsfsample.managedbeans;Please note that we use here BikeDataProvider.java class, which is Spring managed service, the same we used for loading bikes list and loading a certain bike details in previous post.
...
@ManagedBean(name="addBike")
@SessionScoped
public class AddBike implements Serializable {
private static final long serialVersionUID = -2155913853431899821L;
@ManagedProperty("#{bikeDataProvider}")
private BikeDataProvider bikeDataProvider; // injected Spring defined service for bikes
private String name;
private String description;
private String price;
private String discountPrice;
private Integer categoryId;
public String addNewBike(){
Bike newBike = new Bike();
newBike.setName(getName());
newBike.setDescription(getDescription());
newBike.setPrice(Integer.parseInt(getPrice()));
newBike.setDiscountPrice(Integer.parseInt(getDiscountPrice()));
newBike.setCategory(categoryId);
// save new bike and return to the shop
bikeDataProvider.add(newBike);
return "/bikesShop.xhtml";
};
...
}
Now it is time for protected parts of application. I will show two ways of protecting webapp: protecting resources (like access to certain page) and protecting business logic methods execution. Scenario 1 will be an example of protecting business logic and scenario 2 will be an example of protecting resources.
When user tries to access the protected area (resource or invoke protected method), application will check user roles and based on them will decide if let the user go further or force him to log in. Log in - that's right - a login page will be displayed where user will enter his credentials. Based on them Spring Security will decide what roles user has and depends on assigned roles further action will be continued or not. Let's modify our application to use Spring Security:
Step 1. Modify configuration files:
applicationContext.xml source:
...Access-denied-page is invoked when user is authenticated but is not authorized to access protected resources. When user is not authenticated, he is moved into form-login instead of access-denied-page.
<!--
resource security
-->
<sec:http auto-config="true" access-denied-page="/faces/accessDenied.xhtml">
<sec:form-login login-page="/faces/login.xhtml" />
<sec:intercept-url pattern="/faces/admin/**" access="ROLE_ADMIN" />
</sec:http>
<!--
business logic (method) security
-->
<sec:global-method-security
secured-annotations="enabled" jsr250-annotations="enabled" >
</sec:global-method-security>
<!--
manager responsible for loading user account with assigned roles
-->
<sec:authentication-manager alias="authenticationManager">
<sec:authentication-provider
user-service-ref="userDetailsService" />
</sec:authentication-manager>
...
web.xml source:
...Step 2. Additional pages: login.xhtml and accessDenied.xhtml.
<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>
...
accessDenied.xhtml is simple page displaying only a message saying that the user is authenticated but still is not authorized to go further.
login.xhtml is a simple page with login form where user enters his credentials (login and password). The more interesting part is corresponding managed bean LoginBean.java which uses a Spring service for authenticating users:
package com.jsfsample.managedbeans;When login was successful and user is authenticated, he is moved to the shop. If not a proper message is displayed and user can re-enter his credentials or go back to the shop without login. Let's look inside AuthenticationService.java class which is a service deciding if user is authenticated or not.
...
@ManagedBean(name = "loginBean")
@SessionScoped
public class LoginBean implements Serializable {
private static final long serialVersionUID = 1L;
private String login;
private String password;
@ManagedProperty(value = "#{authenticationService}")
private AuthenticationService authenticationService; // injected Spring defined service for bikes
public String login() {
boolean success = authenticationService.login(login, password);
if (success){
return "bikesShop.xhtml"; // return to application but being logged now
}
else{
FacesContext.getCurrentInstance().addMessage(null, new FacesMessage("Login or password incorrect."));
return "login.xhtml";
}
}
...
}
Step 3. AuthenticationService implementation:
package com.jsfsample.application.impl;This Spring managed service uses internally a class AuthenticationManager, which comes from Spring Security and was defined as a manager in applicationContext.xml file:
...
@Service("authenticationService")
public class AuthenticationServiceImpl implements com.jsfsample.application.AuthenticationService {
@Resource(name = "authenticationManager")
private AuthenticationManager authenticationManager; // specific for Spring Security
@Override
public boolean login(String username, String password) {
try {
Authentication authenticate = authenticationManager
.authenticate(new UsernamePasswordAuthenticationToken(
username, password));
if (authenticate.isAuthenticated()) {
SecurityContextHolder.getContext().setAuthentication(
authenticate);
return true;
}
} catch (AuthenticationException e) {
}
return false;
}
...
}
...Note that we do not explicit define AuthenticationManager! It is a ready to use object. But AuthenticationManager has helper service named userDetailService defined in applicationContext.xml file - this service must be written by our own.
<!--
manager responsible for loading user account with assigned roles
-->
<sec:authentication-manager alias="authenticationManager">
<sec:authentication-provider
user-service-ref="userDetailsService" />
</sec:authentication-manager>
...
Step 4. Implementation of userDetailService.
userDetailsService source code is shown below:
package com.jsfsample.application.impl;We have here a Spring Security specific objects representing users and roles. In the init() method I created some mocked data two roles representing page administrators and registered users - ROLE_ADMIN and ROLE_REGISTERED. For each role I created a single account: admin (password: admin) for the ROLE_ADMIN and user (password: user) for the ROLE_REGISTERED. That's all - it is time to protect applicartion.
...
@Service("userDetailsService")
public class UserDetailsServiceImpl implements UserDetailsService {
private HashMapusers = new HashMap ();
@Override
public UserDetails loadUserByUsername(String username)
throws UsernameNotFoundException{
org.springframework.security.core.userdetails.User user = users.get(username);
if (user == null) {
throw new UsernameNotFoundException("UserAccount for name \""
+ username + "\" not found.");
}
return user;
}
@PostConstruct
public void init() {
// sample roles
CollectionadminAuthorities = new ArrayList ();
adminAuthorities.add(new GrantedAuthorityImpl("ROLE_ADMIN"));
CollectionuserAuthorities = new ArrayList ();
userAuthorities.add(new GrantedAuthorityImpl("ROLE_REGISTERED"));
boolean enabled = true;
boolean accountNonExpired = true;
boolean credentialsNonExpired = true;
boolean accountNonLocked = true;
// sample users with roles set
users.put("admin", new org.springframework.security.core.userdetails.User("admin", "admin", enabled, accountNonExpired,
credentialsNonExpired, accountNonLocked, adminAuthorities));
users.put("user", new org.springframework.security.core.userdetails.User("user", "user", enabled, accountNonExpired,
credentialsNonExpired, accountNonLocked, userAuthorities));
}
}
Step 5. Protecting application.
5 a) Scenario 1: protecting business logic. We have to protect invoking a method which allows to see bike details. This method is placed inside BikeDataProvider.java service class. In order to protect the method we have to add an annotation defining roles allowed to execute this method:
package com.jsfsample.services;This simply means that only registered users or admin users can see bike details.
...
public interface BikeDataProvider {
...
@RolesAllowed({"ROLE_ADMIN","ROLE_REGISTERED"})
public abstract Bike getBikeById(Integer id);
public abstract void add(Bike newBike);
}
Why we do not protect the add(...) method? Because we protect the whole page access where this method is executed - of course in addition we can also protect this method by annotating it with @RolesAllowed({"ROLE_ADMIN"}).
5 b) Scenario 2: protecting resource. According to rules of protecting resources defined in applicationContext.xml, we protect all resources which are located inside /admin directory. So we have to create a directory /admin under /WebContent directory and move addBike.xhtml page there. It should look like this:
Note: there is a little trick in the protecting resources like pages in JSF. Spring Security tries to match exact URL address to apply the rule. But in JSF there is a "old URL" issue - after navigtation from page A to page B, URL address in browser still points to page A. In order to make the rule working we have to force the browser to show the current URL instead of old one. It is done by adding a special command into the navigation string returning a page for adding a bike:
public String showForm(){
...
return "/admin/addBike.xhtml?faces-redirect=true";
}
That's all about Spring Security in our sample application.
How to test it? After deploying application on the server and starting the server, we have to open a browser and type in URL:
Then try to display some bike details. When promped for login, enter credentials: user, user and try again. Then try to add a new bike - You should see access denied page. The close the application and clean the browser cache and try the same with the user admin, admin.
http://localhost:8080/JSF2FeaturesSpring
Then try to display some bike details. When promped for login, enter credentials: user, user and try again. Then try to add a new bike - You should see access denied page. The close the application and clean the browser cache and try the same with the user admin, admin.
-------------------------------------------
Download source files:
Note: make sure that Java, Eclipse and Tomcat are properly installed and configured for running the project (additional configuration may be required if different directories are used).
Eclipse complete sample project is here (with all required libraries). The sample project is a ready to run application which contains all described Spring Security issues in this post. You can also download a war file located here (just copy it inside webapps folder in Your Tomcat and start Tomcat with the script startup.bat)